feat: add article and production modules

This commit is contained in:
2026-08-31 13:16:46 +08:00
parent ac3da246b2
commit 5c586a1a95
334 changed files with 34419 additions and 1 deletions
@@ -0,0 +1,519 @@
import request from "@/utils/http";
/**
* produce 插件 API
*/
import BaseService from "@/api/core/base";
export interface ProduceProcessRow {
id?: string | number;
process_code: string;
process_name: string;
description?: string;
step_count?: number;
sort?: number;
status?: number;
created_date?: string;
updated_date?: string;
}
export interface ProduceProcessStepRow {
id?: string | number;
process_id: string | number;
step_code: string;
step_name: string;
description?: string;
sort?: number;
status?: number;
prerequisite_steps?: Array<string | number> | string | null;
prerequisite_mode?: "all" | "any" | "none";
prerequisite_require_final?: number;
allow_repeat_scan?: number;
repeat_scan_statuses?: Array<string | number> | string | null;
process?: {
id?: string | number;
process_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStepStatusRow {
id?: string | number;
step_id: string | number;
status_code: string;
status_name: string;
is_initial?: number;
is_final?: number;
description?: string;
sort?: number;
status?: number;
step?: {
id?: string | number;
step_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStepRelationRow {
id?: string | number;
from_step_id: string | number;
to_step_id: string | number;
relation_type: "normal" | "exception";
flow_condition?: string | Record<string, any> | null;
priority?: number;
description?: string;
sort?: number;
status?: number;
from_step?: {
id?: string | number;
step_name?: string;
};
to_step?: {
id?: string | number;
step_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStatusTransitionRow {
id?: string | number;
step_id: string | number;
from_status_id: string | number;
to_status_id: string | number;
trigger_event: string;
event_config?: string | Record<string, any> | null;
auto_trigger?: number;
priority?: number;
transition_type?: "primary" | "side_effect";
side_effect_type?: string;
side_effect_config?: string | Record<string, any> | null;
description?: string;
sort?: number;
status?: number;
step?: {
id?: string | number;
step_name?: string;
};
from_status?: {
id?: string | number;
status_name?: string;
};
to_status?: {
id?: string | number;
status_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceMaterialCategoryRow {
id?: string | number;
category_name: string;
created_date?: string;
updated_date?: string;
}
export interface ProduceMaterialRow {
id?: string | number;
category_id: string | number;
category_name?: string;
product_code: string;
product_name: string;
created_date?: string;
updated_date?: string;
}
export interface ProduceHardwareFaultDescriptionRow {
id?: string | number;
// ... other properties
}
export const ProduceMaterialCategoryService = {
...BaseService<ProduceMaterialCategoryRow>({
baseUrl: "/produce/material-category",
}),
};
export const ProduceMaterialService = {
...BaseService<ProduceMaterialRow>({ baseUrl: "/produce/material" }),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
importExcel(data: FormData) {
return request.post<{ created: number; updated: number }>({
url: "/produce/material/import",
data,
headers: { "Content-Type": "multipart/form-data" },
});
},
};
export const ProduceProcessService = {
...BaseService<ProduceProcessRow>({ baseUrl: "/produce/process" }),
};
export const ProduceProcessStepService = {
...BaseService<ProduceProcessStepRow>({ baseUrl: "/produce/process-step" }),
};
export const ProduceStepStatusService = {
...BaseService<ProduceStepStatusRow>({ baseUrl: "/produce/step-status" }),
};
export const ProduceStepRelationService = {
...BaseService<ProduceStepRelationRow>({ baseUrl: "/produce/step-relation" }),
};
export const ProduceStatusTransitionService = {
...BaseService<ProduceStatusTransitionRow>({
baseUrl: "/produce/status-transition",
}),
};
export interface ProduceTransitionLogRow {
id?: string | number;
work_order_id?: string | number | null;
work_order_no?: string;
step_id: string | number;
transition_id?: string | number | null;
from_status_id: string | number;
to_status_id: string | number;
trigger_event: string;
trigger_data?: Record<string, any> | string | null;
trigger_user?: string;
trigger_ip?: string;
is_auto?: number;
is_success?: number;
error_message?: string | null;
duration?: number;
remark?: string | null;
created_by?: string | number | null;
created_name?: string;
created_date?: string;
step?: {
id?: string | number;
step_name?: string;
};
transition?: {
id?: string | number;
trigger_event?: string;
};
from_status?: {
id?: string | number;
status_name?: string;
};
to_status?: {
id?: string | number;
status_name?: string;
};
}
export const ProduceTransitionLogService = {
...BaseService<ProduceTransitionLogRow>({
baseUrl: "/produce/transition-log",
allowedMethods: ["list", "get", "export"],
}),
};
export interface ProduceSpecificRuleRow {
id?: string | number;
rule_name: string;
rule_pattern: string;
rule_type: number;
apply_mode?: "order" | "machine" | "both";
match_field?: "order_no" | "process_order_no" | "product_code" | "user_code";
is_enabled?: number;
step_ids?: Array<string | number> | string | null;
sort?: number;
remark?: string;
created_name?: string;
created_date?: string;
}
export interface ProduceProductRecordRow {
id?: string | number;
process_order_no: string;
product_code: string;
quantity: number;
process_id?: string | number | null;
step_id?: string | number | null;
status_id?: string | number | null;
user_code?: string;
operator?: string;
operator_id?: string | number | null;
scan_result?: number;
remark?: string | null;
process?: {
id?: string | number;
process_name?: string;
};
step?: {
id?: string | number;
step_name?: string;
};
status?: {
id?: string | number;
status_name?: string;
};
created_name?: string;
created_date?: string;
}
export interface ProduceScanDataRow {
id?: string | number;
order_no: string;
quantity?: number;
user_code: string;
operator?: string;
operator_id?: string | number | null;
process_id: string | number;
step_id: string | number;
status_id?: string | number | null;
scan_result: number;
scan_time?: number;
scan_date?: string;
remark?: string | null;
process?: {
id?: string | number;
process_name?: string;
};
step?: {
id?: string | number;
step_name?: string;
};
status?: {
id?: string | number;
status_name?: string;
};
created_name?: string;
created_date?: string;
}
export const ProduceSpecificRuleService = {
...BaseService<ProduceSpecificRuleRow>({ baseUrl: "/produce/specific-rule" }),
};
export const ProduceProductRecordService = {
...BaseService<ProduceProductRecordRow>({
baseUrl: "/produce/product-record",
}),
};
export const ProduceScanDataService = {
...BaseService<ProduceScanDataRow>({ baseUrl: "/produce/scan-data" }),
};
export interface ProduceExceptionSettingRow {
id?: string | number;
parent_id?: string | number | null;
parent_name?: string;
setting_type: "type" | "reason" | "issue";
code: string;
name: string;
sort?: number;
is_enabled?: number;
remark?: string | null;
created_name?: string;
created_date?: string;
}
export interface ProduceExceptionRecordRow {
id?: string | number;
order_no: string;
user_code?: string | null;
reporter?: string | null;
type_id?: string | number | null;
type_code: string;
type_name: string;
reason_id?: string | number | null;
reason_code: string;
reason_name: string;
issue_id?: string | number | null;
issue_code?: string | null;
issue_name?: string | null;
images?: string[] | null;
remark?: string | null;
status?: number;
status_name?: string;
handler?: string | null;
handler_id?: string | number | null;
handled_at?: number | null;
handled_date?: string | null;
reported_at?: number | null;
reported_date?: string | null;
created_name?: string;
created_date?: string;
}
export const ProduceExceptionSettingService = {
...BaseService<ProduceExceptionSettingRow>({
baseUrl: "/produce/exception-setting",
}),
};
export interface ProduceRepairSettingRow {
id?: string | number;
parent_id?: string | number | null;
parent_name?: string;
setting_type: "type" | "reason" | "issue";
code: string;
name: string;
sort?: number;
is_enabled?: number;
remark?: string | null;
created_name?: string;
created_date?: string;
}
export interface ProduceHardwareFaultDescriptionRow {
id?: string | number;
category_id: string | number;
category_name?: string;
description: string;
use_count?: number;
is_enabled?: number;
remark?: string | null;
created_date?: string;
}
export interface ProduceRepairRecordRow {
id?: string | number;
repair_no: string;
exception_id?: string | number | null;
exception_reason?: string | null;
exception_issue?: string | null;
exception_type_name?: string | null;
order_no?: string | null;
product_code?: string | null;
product_name?: string | null;
category_id?: string | number | null;
category_name?: string | null;
fault_description_id?: string | number | null;
fault_description_ids?: Array<string | number>;
fault_description?: string | null;
fault_descriptions?: string[];
repair_result?: string | null;
parts?: Array<Record<string, any>> | string | null;
responsible_operators?: Array<Record<string, any>> | string | null;
operator?: string | null;
repaired_at?: number | null;
repaired_date?: string | null;
remark?: string | null;
created_date?: string;
}
export const ProduceRepairSettingService = {
...BaseService<ProduceRepairSettingRow>({
baseUrl: "/produce/repair-setting",
}),
};
export const ProduceHardwareFaultDescriptionService = {
...BaseService<ProduceHardwareFaultDescriptionRow>({
baseUrl: "/produce/hardware-fault-description",
}),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
importExcel(data: FormData) {
return request.post<{ created: number; updated: number }>({
url: "/produce/hardware-fault-description/import",
data,
headers: { "Content-Type": "multipart/form-data" },
});
},
};
export const ProduceRepairRecordService = {
...BaseService<ProduceRepairRecordRow>({ baseUrl: "/produce/repair-record" }),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
faultDescriptionOptions(params: Record<string, any> = {}) {
return ProduceHardwareFaultDescriptionService.list({
limit: 999,
is_enabled: 1,
...params,
});
},
};
export const ProduceExceptionRecordService = {
...BaseService<ProduceExceptionRecordRow>({
baseUrl: "/produce/exception-record",
}),
};
export const ProduceConfigApi = {
info(params: Record<string, any>) {
return request.get({ url: "/produce/config/info", params });
},
save(data: Record<string, any>) {
return request.post({ url: "/produce/config/save", data });
},
};
export default {
materialCategoryList(params = {}) {
return ProduceMaterialCategoryService.list(params as Record<string, any>);
},
materialList(params = {}) {
return ProduceMaterialService.list(params as Record<string, any>);
},
processList(params = {}) {
return ProduceProcessService.list(params as Record<string, any>);
},
processStepList(params = {}) {
return ProduceProcessStepService.list(params as Record<string, any>);
},
stepStatusList(params = {}) {
return ProduceStepStatusService.list(params as Record<string, any>);
},
stepRelationList(params = {}) {
return ProduceStepRelationService.list(params as Record<string, any>);
},
statusTransitionList(params = {}) {
return ProduceStatusTransitionService.list(params as Record<string, any>);
},
transitionLogList(params = {}) {
return ProduceTransitionLogService.list(params as Record<string, any>);
},
specificRuleList(params = {}) {
return ProduceSpecificRuleService.list(params as Record<string, any>);
},
productRecordList(params = {}) {
return ProduceProductRecordService.list(params as Record<string, any>);
},
scanDataList(params = {}) {
return ProduceScanDataService.list(params as Record<string, any>);
},
exceptionRecordList(params = {}) {
return ProduceExceptionRecordService.list(params as Record<string, any>);
},
exceptionSettingList(params = {}) {
return ProduceExceptionSettingService.list(params as Record<string, any>);
},
repairSettingList(params = {}) {
return ProduceRepairSettingService.list(params as Record<string, any>);
},
hardwareFaultDescriptionList(params = {}) {
return ProduceHardwareFaultDescriptionService.list(
params as Record<string, any>,
);
},
repairRecordList(params = {}) {
return ProduceRepairRecordService.list(params as Record<string, any>);
},
};
@@ -0,0 +1,50 @@
import request from '@/utils/http'
export interface WorkStatisticsQuery {
month?: string
date?: string
data_type?: string
process_id?: string | number
step_id?: string | number
scan_result?: string | number
user_code?: string
operator?: string
process_name?: string
step_name?: string
}
export interface WorkStatisticsColumn {
key: string
label: string
}
export interface WorkStatisticsOption {
label: string
value: string | number
}
export interface WorkStatisticsResponse {
columns: WorkStatisticsColumn[]
items: Array<Record<string, string | number | null>>
}
export const ProduceWorkStatisticsService = {
monthly(params: WorkStatisticsQuery = {}) {
return request.get<WorkStatisticsResponse>({ url: '/produce/statistics/monthly', params })
},
daily(params: WorkStatisticsQuery = {}) {
return request.get<WorkStatisticsResponse>({ url: '/produce/statistics/daily', params })
},
getProcessOptions() {
return request.get<WorkStatisticsOption[]>({
url: '/produce/process',
params: { format: 'select' }
})
},
getStepOptions(processId?: string | number) {
return request.get<WorkStatisticsOption[]>({
url: '/produce/process-step',
params: { format: 'select', process_id: processId || undefined }
})
}
}
@@ -0,0 +1,217 @@
{
"produce": {
"common": {
"columns": {
"index": "No."
}
},
"modules": {
"process": {
"title": "Process Management",
"search": {
"process_code": "Process Code",
"process_code_placeholder": "Please enter process code",
"process_name": "Process Name",
"process_name_placeholder": "Please enter process name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"process_code_placeholder": "Please enter process code",
"process_name_placeholder": "Please enter process name",
"description": "Process Description",
"description_placeholder": "Please enter process description"
},
"columns": {
"process_code": "Process Code",
"process_name": "Process Name",
"step_count": "Step Count",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"process_step": {
"title": "Process Steps",
"search": {
"process_id": "Process",
"process_id_placeholder": "Please select process",
"step_code": "Step Code",
"step_code_placeholder": "Please enter step code",
"step_name": "Step Name",
"step_name_placeholder": "Please enter step name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"process_id": "Process",
"process_id_placeholder": "Please select process",
"step_code_placeholder": "Please enter step code",
"step_name_placeholder": "Please enter step name",
"description": "Step Description",
"description_placeholder": "Please enter step description"
},
"columns": {
"process_name": "Process",
"step_code": "Step Code",
"step_name": "Step Name",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"step_status": {
"title": "Step Status",
"search": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"status_code": "Status Code",
"status_code_placeholder": "Please enter status code",
"status_name": "Status Name",
"status_name_placeholder": "Please enter status name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"status_code_placeholder": "Please enter status code",
"status_name_placeholder": "Please enter status name",
"description": "Status Description",
"description_placeholder": "Please enter status description"
},
"columns": {
"step_name": "Step",
"status_code": "Status Code",
"status_name": "Status Name",
"is_initial": "Initial",
"is_final": "Final",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"step_relation": {
"title": "Step Relation",
"search": {
"from_step_id": "From Step",
"from_step_id_placeholder": "Please select from step",
"to_step_id": "To Step",
"to_step_id_placeholder": "Please select to step",
"relation_type": "Relation Type",
"relation_type_placeholder": "Please select relation type",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"from_step_id": "From Step",
"from_step_id_placeholder": "Please select from step",
"to_step_id": "To Step",
"to_step_id_placeholder": "Please select to step",
"flow_condition": "Flow Condition",
"flow_condition_placeholder": "Please enter JSON flow condition",
"description": "Relation Description",
"description_placeholder": "Please enter relation description"
},
"columns": {
"from_step_name": "From Step",
"to_step_name": "To Step",
"relation_type": "Relation Type",
"priority": "Priority",
"flow_condition": "Flow Condition",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"status_transition": {
"title": "Status Transition",
"search": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"trigger_event": "Trigger Event",
"trigger_event_placeholder": "Please select trigger event",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"from_status_id": "From Status",
"from_status_id_placeholder": "Please select from status",
"to_status_id": "To Status",
"to_status_id_placeholder": "Please select to status",
"trigger_event_placeholder": "Please select trigger event",
"side_effect_type": "Side Effect Type",
"side_effect_type_placeholder": "Please select side effect type",
"event_config": "Event Config",
"event_config_placeholder": "Please enter JSON event config",
"side_effect_config": "Side Effect Config",
"side_effect_config_placeholder": "Please enter JSON side effect config",
"description": "Transition Description",
"description_placeholder": "Please enter transition description"
},
"columns": {
"step_name": "Step",
"from_status_name": "From Status",
"to_status_name": "To Status",
"trigger_event": "Trigger Event",
"transition_type": "Transition Type",
"auto_trigger": "Auto Trigger",
"priority": "Priority",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"transition_log": {
"title": "Transition Log",
"search": {
"work_order_no": "Work Order No.",
"work_order_no_placeholder": "Please enter work order no",
"step_id": "Step",
"step_id_placeholder": "Please select step",
"trigger_event": "Trigger Event",
"trigger_event_placeholder": "Please select trigger event",
"trigger_user": "Trigger User",
"trigger_user_placeholder": "Please enter trigger user",
"is_success": "Result",
"is_success_placeholder": "Please select result"
},
"columns": {
"work_order_no": "Work Order No",
"step_name": "Step",
"from_status_name": "From Status",
"to_status_name": "To Status",
"transition_event": "Transition Rule",
"trigger_event": "Trigger Event",
"trigger_user": "Trigger User",
"trigger_ip": "Trigger IP",
"is_auto": "Auto Trigger",
"is_success": "Result",
"duration": "Duration(ms)",
"trigger_data": "Trigger Data",
"error_message": "Error Message",
"remark": "Remark",
"created_name": "Created By",
"created_at": "Created At"
}
},
"config": {
"title": "Production Config"
},
"scan_data": {
"title": "Scan Data"
},
"exception_data": {
"title": "Exception Records"
},
"exception_setting": {
"title": "Exception Settings"
},
"exception_analysis": {
"title": "Exception Analysis"
}
}
}
}
@@ -0,0 +1,217 @@
{
"produce": {
"common": {
"columns": {
"index": "序号"
}
},
"modules": {
"process": {
"title": "工序管理",
"search": {
"process_code": "工序编码",
"process_code_placeholder": "请输入工序编码",
"process_name": "工序名称",
"process_name_placeholder": "请输入工序名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"process_code_placeholder": "请输入工序编码",
"process_name_placeholder": "请输入工序名称",
"description": "工序描述",
"description_placeholder": "请输入工序描述"
},
"columns": {
"process_code": "工序编码",
"process_name": "工序名称",
"step_count": "步骤数",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"process_step": {
"title": "工序步骤",
"search": {
"process_id": "所属工序",
"process_id_placeholder": "请选择所属工序",
"step_code": "步骤编码",
"step_code_placeholder": "请输入步骤编码",
"step_name": "步骤名称",
"step_name_placeholder": "请输入步骤名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"process_id": "所属工序",
"process_id_placeholder": "请选择所属工序",
"step_code_placeholder": "请输入步骤编码",
"step_name_placeholder": "请输入步骤名称",
"description": "步骤描述",
"description_placeholder": "请输入步骤描述"
},
"columns": {
"process_name": "所属工序",
"step_code": "步骤编码",
"step_name": "步骤名称",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"step_status": {
"title": "步骤状态",
"search": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"status_code": "状态编码",
"status_code_placeholder": "请输入状态编码",
"status_name": "状态名称",
"status_name_placeholder": "请输入状态名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"status_code_placeholder": "请输入状态编码",
"status_name_placeholder": "请输入状态名称",
"description": "状态描述",
"description_placeholder": "请输入状态描述"
},
"columns": {
"step_name": "所属步骤",
"status_code": "状态编码",
"status_name": "状态名称",
"is_initial": "初始状态",
"is_final": "终态",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"step_relation": {
"title": "步骤关系",
"search": {
"from_step_id": "源步骤",
"from_step_id_placeholder": "请选择源步骤",
"to_step_id": "目标步骤",
"to_step_id_placeholder": "请选择目标步骤",
"relation_type": "关系类型",
"relation_type_placeholder": "请选择关系类型",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"from_step_id": "源步骤",
"from_step_id_placeholder": "请选择源步骤",
"to_step_id": "目标步骤",
"to_step_id_placeholder": "请选择目标步骤",
"flow_condition": "流转条件",
"flow_condition_placeholder": "请输入 JSON 格式流转条件",
"description": "关系描述",
"description_placeholder": "请输入关系描述"
},
"columns": {
"from_step_name": "源步骤",
"to_step_name": "目标步骤",
"relation_type": "关系类型",
"priority": "优先级",
"flow_condition": "流转条件",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"status_transition": {
"title": "状态转换",
"search": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"trigger_event": "触发事件",
"trigger_event_placeholder": "请选择触发事件",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"from_status_id": "源状态",
"from_status_id_placeholder": "请选择源状态",
"to_status_id": "目标状态",
"to_status_id_placeholder": "请选择目标状态",
"trigger_event_placeholder": "请选择触发事件",
"side_effect_type": "副作用类型",
"side_effect_type_placeholder": "请选择副作用类型",
"event_config": "事件配置",
"event_config_placeholder": "请输入 JSON 格式事件配置",
"side_effect_config": "副作用配置",
"side_effect_config_placeholder": "请输入 JSON 格式副作用配置",
"description": "转换描述",
"description_placeholder": "请输入转换描述"
},
"columns": {
"step_name": "所属步骤",
"from_status_name": "源状态",
"to_status_name": "目标状态",
"trigger_event": "触发事件",
"transition_type": "转换类型",
"auto_trigger": "自动触发",
"priority": "优先级",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"transition_log": {
"title": "转换日志",
"search": {
"work_order_no": "工单编号",
"work_order_no_placeholder": "请输入工单编号",
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"trigger_event": "触发事件",
"trigger_event_placeholder": "请选择触发事件",
"trigger_user": "触发用户",
"trigger_user_placeholder": "请输入触发用户",
"is_success": "执行结果",
"is_success_placeholder": "请选择执行结果"
},
"columns": {
"work_order_no": "工单编号",
"step_name": "所属步骤",
"from_status_name": "源状态",
"to_status_name": "目标状态",
"transition_event": "转换规则",
"trigger_event": "触发事件",
"trigger_user": "触发用户",
"trigger_ip": "触发IP",
"is_auto": "自动触发",
"is_success": "执行结果",
"duration": "耗时(ms)",
"trigger_data": "触发数据",
"error_message": "错误信息",
"remark": "备注",
"created_name": "创建人",
"created_at": "创建时间"
}
},
"config": {
"title": "生产配置"
},
"scan_data": {
"title": "扫码数据"
},
"exception_data": {
"title": "异常数据"
},
"exception_setting": {
"title": "异常设置"
},
"exception_analysis": {
"title": "异常数据分析"
}
}
}
}
@@ -0,0 +1,134 @@
<?php
/**
* 后台菜单配置
* 安装时会自动加载
*/
return [
[
'name' => '生产管理',
'code' => 'produce',
'app' => 'produce',
'path' => '/produce',
'icon' => 'plugin',
'children' => [
[
'name' => '生产配置',
'code' => 'produce:config:index',
'app' => 'produce',
'path' => '/produce/config',
'component' => '/produce/config/index',
],
[
'name' => '工序管理',
'code' => 'produce:process:index',
'app' => 'produce',
'path' => '/produce/process',
'component' => '/produce/process/index',
],
[
'name' => '工序步骤',
'code' => 'produce:process-step:index',
'app' => 'produce',
'path' => '/produce/process-step',
'component' => '/produce/process-step/index',
],
[
'name' => '步骤状态',
'code' => 'produce:step-status:index',
'app' => 'produce',
'path' => '/produce/step-status',
'component' => '/produce/step-status/index',
],
[
'name' => '步骤关系',
'code' => 'produce:step-relation:index',
'app' => 'produce',
'path' => '/produce/step-relation',
'component' => '/produce/step-relation/index',
],
[
'name' => '状态转换',
'code' => 'produce:status-transition:index',
'app' => 'produce',
'path' => '/produce/status-transition',
'component' => '/produce/status-transition/index',
],
[
'name' => '整机数据',
'code' => 'produce:product-record:index',
'app' => 'produce',
'path' => '/produce/product-record',
'component' => '/produce/product-record/index',
],
[
'name' => '扫码数据',
'code' => 'produce:scan-data:index',
'app' => 'produce',
'path' => '/produce/scan-data',
'component' => '/produce/scan-data/index',
],
[
'name' => '转换日志',
'code' => 'produce:transition-log:index',
'app' => 'produce',
'path' => '/produce/transition-log',
'component' => '/produce/transition-log/index',
],
[
'name' => '异常管理',
'code' => 'produce:exception:index',
'app' => 'produce',
'path' => '/produce/exception',
'icon' => 'warning',
'children' => [
[
'name' => '异常数据',
'code' => 'produce:exception-data:list',
'app' => 'produce',
'path' => '/produce/exception-data',
'component' => '/produce/exception/data/index',
],
[
'name' => '异常设置',
'code' => 'produce:exception-setting:list',
'app' => 'produce',
'path' => '/produce/exception-setting',
'component' => '/produce/exception/setting/index',
],
[
'name' => '数据分析',
'code' => 'produce:exception-analysis:read',
'app' => 'produce',
'path' => '/produce/exception-analysis',
'component' => '/produce/exception/analysis/index',
],
],
],
[
'name' => '数据统计',
'code' => 'produce:statistics:index',
'app' => 'produce',
'path' => '/produce/statistics',
'icon' => 'bar-chart-2',
'children' => [
[
'name' => '月工作统计',
'code' => 'produce:statistics:monthly',
'app' => 'produce',
'path' => '/produce/statistics/monthly',
'component' => '/produce/statistics/monthly/index',
],
[
'name' => '日工作统计',
'code' => 'produce:statistics:daily',
'app' => 'produce',
'path' => '/produce/statistics/daily',
'component' => '/produce/statistics/daily/index',
],
],
],
],
],
];
@@ -0,0 +1,215 @@
import { AppRouteRecord } from "@/types/router";
import { RoutesAlias } from "@/router/routesAlias";
export default {
path: "/produce",
name: "Produce",
component: RoutesAlias.Layout,
meta: {
title: "生产管理",
icon: "ri:plugin-line",
module: "produce",
},
children: [
{
path: "material-category",
name: "ProduceMaterialCategory",
component: "/produce/material-category/index",
meta: {
title: "物料分类",
icon: "ri:folder-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "material",
name: "ProduceMaterial",
component: "/produce/material/index",
meta: {
title: "物料库",
icon: "ri:archive-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "config",
name: "ProduceConfig",
component: "/produce/config/index",
meta: {
title: "生产配置",
icon: "ri:settings-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "process",
name: "ProduceProcess",
component: "/produce/process/index",
meta: {
title: "工序管理",
icon: "ri:stack-line",
keepAlive: false,
module: "produce",
},
},
{
path: "process-step",
name: "ProduceProcessStep",
component: "/produce/process-step/index",
meta: {
title: "工序步骤",
icon: "ri:list-check-3",
keepAlive: false,
module: "produce",
},
},
{
path: "step-status",
name: "ProduceStepStatus",
component: "/produce/step-status/index",
meta: {
title: "步骤状态",
icon: "ri:flag-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "step-relation",
name: "ProduceStepRelation",
component: "/produce/step-relation/index",
meta: {
title: "步骤关系",
icon: "ri:git-branch-line",
keepAlive: false,
module: "produce",
},
},
{
path: "status-transition",
name: "ProduceStatusTransition",
component: "/produce/status-transition/index",
meta: {
title: "状态转换",
icon: "ri:share-forward-line",
keepAlive: false,
module: "produce",
},
},
{
path: "product-record",
name: "ProduceProductRecord",
component: "/produce/product-record/index",
meta: {
title: "整机数据",
icon: "ri-product-hunt-line",
keepAlive: false,
module: "produce",
},
},
{
path: "scan-data",
name: "ProduceScanData",
component: "/produce/scan-data/index",
meta: {
title: "扫码数据",
icon: "ri:qr-scan-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "transition-log",
name: "ProduceTransitionLog",
component: "/produce/transition-log/index",
meta: {
title: "转换日志",
icon: "ri:file-list-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "repair",
name: "ProduceRepair",
meta: {
title: "维修管理",
icon: "ri:tools-line",
keepAlive: false,
module: "produce",
},
children: [
{
path: "repair-data",
name: "ProduceRepairData",
component: "/produce/repair/repair-data/index",
meta: {
title: "维修数据",
icon: "ri:file-list-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "repair-setting",
name: "ProduceRepairSetting",
component: "/produce/repair/repair-setting/index",
meta: {
title: "维修设置",
icon: "ri:settings-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "hardware-fault-description",
name: "ProduceHardwareFaultDescription",
component: "/produce/repair/hardware-fault-description/index",
meta: {
title: "硬件故障描述",
icon: "ri:tools-line",
keepAlive: false,
module: "produce",
},
},
],
},
{
path: "statistics",
name: "ProduceStatistics",
meta: {
title: "数据统计",
icon: "ri:bar-chart-2-line",
keepAlive: false,
module: "produce",
},
children: [
{
path: "monthly",
name: "ProduceMonthlyWorkStatistics",
component: "/produce/statistics/monthly/index",
meta: {
title: "月工作统计",
icon: "ri:calendar-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "daily",
name: "ProduceDailyWorkStatistics",
component: "/produce/statistics/daily/index",
meta: {
title: "日工作统计",
icon: "ri:time-line",
keepAlive: false,
module: "produce",
},
},
],
},
],
} as AppRouteRecord;
@@ -0,0 +1,52 @@
<template>
<div class="page-content" style="margin-bottom: 20px">
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="item in settingList" :key="item.key" :name="item.key" :label="item.name">
<component :is="item.component" :key="item.loadKey" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="tsx">
import { defineAsyncComponent, markRaw, ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const activeName = ref('basic-setting')
const handleClick = (tab: TabsPaneContext) => {
const targetKey = String(tab.props.name)
settingList.value.forEach((item) => {
if (item.key === targetKey) {
item.loadKey += 1
}
})
}
const settingList = ref([
{
component: markRaw(defineAsyncComponent(() => import('./modules/basic-setting.vue'))),
key: 'basic-setting',
name: '基础设置',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
key: 'step-rule',
name: '步骤规则',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/specific-rule.vue'))),
key: 'specific-rule',
name: '特定规则',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/erp-integration.vue'))),
key: 'erp-integration',
name: 'ERP 集成',
loadKey: 0
}
])
</script>
@@ -0,0 +1,175 @@
<template>
<div>
<BasicForm />
<el-button class="site-sumbit" type="primary" @click="onSubmit">保存配置</el-button>
</div>
</template>
<script setup lang="tsx">
import { useForm } from '@/components/form'
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { ProduceConfigApi } from '@/apps/produce/api'
const toBoolean = (value: unknown) => value === 1 || value === '1' || value === true
const normalizeData = (data: Record<string, any>) => {
return {
...data,
global_allow_repeat: toBoolean(data.global_allow_repeat) ? 1 : 0,
recent_records_today_only: toBoolean(data.recent_records_today_only) ? 1 : 0
}
}
const [BasicForm, formApi] = useForm({
labelPosition: 'right',
labelWidth: '160px',
inline: false,
schema: [
{
prop: 'order_no_regex',
label: '订单号验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入订单号验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'order_no_min_length',
label: '订单号最小长度',
component: 'InputNumber',
defaultValue: 8,
componentProps: {
min: 1,
max: 64,
class: 'w-full'
}
},
{
prop: 'process_order_no_regex',
label: '加工单号验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入加工单号验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'user_code_regex',
label: '用户编码验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入用户编码验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'product_code_regex',
label: '商品编码验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入商品编码验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'product_code_min_length',
label: '商品编码最小长度',
component: 'InputNumber',
defaultValue: 6,
componentProps: {
min: 1,
max: 64,
class: 'w-full'
}
},
{
prop: 'product_code_max_length',
label: '商品编码最大长度',
component: 'InputNumber',
defaultValue: 20,
componentProps: {
min: 1,
max: 128,
class: 'w-full'
}
},
{
prop: 'global_allow_repeat',
label: '全局允许重复',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 }
]
}
},
{
prop: 'repeat_scan_interval',
label: '重复扫码最小间隔(秒)',
component: 'InputNumber',
defaultValue: 0,
componentProps: {
min: 0,
max: 86400,
class: 'w-full'
}
},
{
prop: 'recent_records_today_only',
label: '最近记录只显示当日',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 }
]
}
}
]
})
async function retrieveData() {
const record = (await ProduceConfigApi.info({ group_code: 'basic_setting' })) as Record<string, any>
await formApi.setValues(normalizeData(record || {}))
}
async function onSubmit() {
const values = await formApi.getValues()
await ProduceConfigApi.save({
group_code: 'basic_setting',
configs: [
{ code: 'order_no_regex', name: '订单号验证正则', content: values.order_no_regex ?? '' },
{ code: 'order_no_min_length', name: '订单号最小长度', content: values.order_no_min_length ?? 8 },
{ code: 'process_order_no_regex', name: '加工单号验证正则', content: values.process_order_no_regex ?? '' },
{ code: 'user_code_regex', name: '用户编码验证正则', content: values.user_code_regex ?? '' },
{ code: 'product_code_regex', name: '商品编码验证正则', content: values.product_code_regex ?? '' },
{ code: 'product_code_min_length', name: '商品编码最小长度', content: values.product_code_min_length ?? 6 },
{ code: 'product_code_max_length', name: '商品编码最大长度', content: values.product_code_max_length ?? 20 },
{ code: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) },
{ code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 0 },
{ code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) }
]
})
ElMessage.success('保存成功')
retrieveData()
}
onMounted(() => {
retrieveData()
})
</script>
<style lang="scss">
.site-sumbit {
margin-left: 160px;
}
</style>
@@ -0,0 +1,207 @@
<template>
<div>
<BasicForm />
<el-button class="site-sumbit" type="primary" @click="onSubmit">保存配置</el-button>
</div>
</template>
<script setup lang="tsx">
import { useForm } from '@/components/form'
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { ProduceConfigApi } from '@/apps/produce/api'
const switchKeys = [
'enable_erp_integration',
'validate_erp_order_on_entry',
'validate_on_every_scan',
'skip_order_status_check',
'enable_order_local_cache',
'show_order_items_detail',
'show_order_logistics_info',
'show_data_source_label',
]
const toBoolean = (value: unknown) => value === 1 || value === '1' || value === true
const yesNoOptions = [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
]
const [BasicForm, formApi] = useForm({
labelPosition: 'right',
labelWidth: '180px',
inline: false,
schema: [
{
prop: 'enable_erp_integration',
label: '启用ERP集成',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'validate_erp_order_on_entry',
label: '录入验证ERP订单',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'validate_on_every_scan',
label: '每次扫码都验证',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'skip_order_status_check',
label: '跳过订单状态检查',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'allowed_order_statuses',
label: '允许录入的订单状态',
component: 'Select',
defaultValue: ['Delivering'],
componentProps: {
multiple: true,
clearable: true,
placeholder: '请选择允许录入的订单状态',
options: [
{ label: '发货中', value: 'Delivering' },
{ label: '已付款待审核', value: 'WaitConfirm' },
{ label: '已客审待财审', value: 'WaitFConfirm' },
{ label: '等供销商/外仓发货', value: 'WaitOuterSent' },
{ label: '已发货', value: 'Sent' },
],
},
},
{
prop: 'order_validation_failure_action',
label: '订单验证失败处理',
component: 'RadioGroup',
defaultValue: 'block',
componentProps: {
options: [
{ label: '阻止录入', value: 'block' },
{ label: '警告但允许录入', value: 'warn' },
{ label: '仅记录日志', value: 'log' },
],
},
},
{
prop: 'order_refresh_strategy',
label: '订单刷新策略',
component: 'RadioGroup',
defaultValue: 'smart',
componentProps: {
options: [
{ label: '永不刷新', value: 'never' },
{ label: '智能刷新', value: 'smart' },
{ label: '总是刷新', value: 'always' },
],
},
},
{
prop: 'order_cache_ttl',
label: '缓存有效期(分钟)',
component: 'InputNumber',
defaultValue: 30,
componentProps: {
min: 1,
max: 1440,
class: 'w-full',
},
},
{
prop: 'enable_order_local_cache',
label: '订单数据本地缓存',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_order_items_detail',
label: '显示订单商品明细',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_order_logistics_info',
label: '显示订单物流信息',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_data_source_label',
label: '显示数据来源标识',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
],
})
async function retrieveData() {
const record = ((await ProduceConfigApi.info({ group_code: 'erp_integration' })) || {}) as Record<string, any>
switchKeys.forEach((key) => {
record[key] = toBoolean(record[key]) ? 1 : 0
})
if (typeof record.allowed_order_statuses === 'string') {
try {
record.allowed_order_statuses = JSON.parse(record.allowed_order_statuses)
} catch {
record.allowed_order_statuses = ['Delivering']
}
}
await formApi.setValues(record)
}
async function onSubmit() {
const values = await formApi.getValues()
await ProduceConfigApi.save({
group_code: 'erp_integration',
configs: [
...switchKeys.map((key) => ({ code: key, name: key, content: Number(values[key] ?? 0) })),
{ code: 'allowed_order_statuses', name: 'allowed_order_statuses', content: JSON.stringify(values.allowed_order_statuses || []) },
{ code: 'order_validation_failure_action', name: 'order_validation_failure_action', content: values.order_validation_failure_action || 'block' },
{ code: 'order_refresh_strategy', name: 'order_refresh_strategy', content: values.order_refresh_strategy || 'smart' },
{ code: 'order_cache_ttl', name: 'order_cache_ttl', content: values.order_cache_ttl ?? 30 },
],
})
ElMessage.success('保存成功')
retrieveData()
}
onMounted(() => {
retrieveData()
})
</script>
<style lang="scss">
.site-sumbit {
margin-left: 180px;
}
</style>
@@ -0,0 +1,324 @@
<template>
<div class="produce-specific-rule-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud, type CrudSchema } from '@/components/crud'
import { ProduceSpecificRuleService } from '@/apps/produce/api'
defineOptions({ name: 'ProduceSpecificRuleList' })
const normalizeStepIds = (value: unknown) => {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const applyModeOptions = [
{ label: '订单模式', value: 'order' },
{ label: '整机模式', value: 'machine' },
{ label: '全部模式', value: 'both' },
]
const matchFieldOptions = [
{ label: '订单号', value: 'order_no' },
{ label: '加工单号', value: 'process_order_no' },
{ label: '商品编码', value: 'product_code' },
{ label: '用户编码', value: 'user_code' },
]
const crudSchema = (): CrudSchema => {
return {
api: ProduceSpecificRuleService.list,
dialogTitle: '特定规则',
crudApi: {
add: (params: any) => ProduceSpecificRuleService.create({
...params,
apply_mode: params?.apply_mode || 'order',
match_field: params?.match_field || 'order_no',
step_ids: normalizeStepIds(params?.step_ids),
}),
edit: (params: any) => ProduceSpecificRuleService.update({
...params,
apply_mode: params?.apply_mode || 'order',
match_field: params?.match_field || 'order_no',
step_ids: normalizeStepIds(params?.step_ids),
}),
remove: ProduceSpecificRuleService.remove,
view: async (id: string | number) => {
const res = await ProduceSpecificRuleService.get(id)
return {
...res,
apply_mode: res?.apply_mode || 'order',
match_field: res?.match_field || 'order_no',
step_ids: normalizeStepIds(res?.step_ids),
}
},
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:specific-rule:create',
editAuth: 'produce:specific-rule:update',
removeAuth: 'produce:specific-rule:delete',
viewAuth: 'produce:specific-rule:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_rule_name',
label: '规则名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规则名称',
},
colSpan: 8,
},
{
prop: 'apply_mode',
label: '适用模式',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择适用模式',
options: applyModeOptions,
},
colSpan: 8,
},
{
prop: 'rule_type',
label: '规则类型',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择规则类型',
options: [
{ label: '允许重复', value: 1 },
{ label: '禁止重复', value: 2 },
],
},
colSpan: 8,
},
{
prop: 'is_enabled',
label: '状态',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择状态',
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'rule_name', label: '规则名称', minWidth: 180 },
{
prop: 'apply_mode',
label: '适用模式',
minWidth: 120,
formatter: (row: any) => (row.apply_mode === 'machine' ? '整机模式' : row.apply_mode === 'both' ? '全部模式' : '订单模式'),
},
{
prop: 'match_field',
label: '匹配字段',
minWidth: 120,
formatter: (row: any) => (row.match_field === 'process_order_no' ? '加工单号' : row.match_field === 'product_code' ? '商品编码' : row.match_field === 'user_code' ? '用户编码' : '订单号'),
},
{ prop: 'rule_pattern', label: '正则表达式', minWidth: 220 },
{
prop: 'rule_type',
label: '规则类型',
minWidth: 120,
formatter: (row: any) => (row.rule_type === 2 ? '禁止重复' : '允许重复'),
},
{
prop: 'step_ids',
label: '适用步骤',
minWidth: 140,
formatter: (row: any) => {
const value = row.step_ids
if (!value || (Array.isArray(value) && value.length === 0)) return '全部步骤'
if (typeof value === 'string') {
try {
const list = JSON.parse(value)
return Array.isArray(list) && list.length > 0 ? `已指定 ${list.length} 个步骤` : '全部步骤'
} catch {
return '全部步骤'
}
}
return Array.isArray(value) && value.length > 0 ? `已指定 ${value.length} 个步骤` : '全部步骤'
},
},
{
prop: 'is_enabled',
label: '状态',
width: 100,
formatter: (row: any) => (row.is_enabled === 0 ? '禁用' : '启用'),
},
{ prop: 'sort', label: '排序', width: 100 },
{ prop: 'remark', label: '备注', minWidth: 180 },
],
dialogProps: {
width: '820px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'rule_name',
label: '规则名称',
componentProps: {
placeholder: '请输入规则名称',
maxlength: 255,
},
rules: [{ required: true, message: '请输入规则名称' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'apply_mode',
label: '适用模式',
defaultValue: 'order',
componentProps: {
options: applyModeOptions,
},
rules: [{ required: true, message: '请选择适用模式' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'rule_type',
label: '规则类型',
defaultValue: 1,
componentProps: {
options: [
{ label: '允许重复', value: 1 },
{ label: '禁止重复', value: 2 },
],
},
rules: [{ required: true, message: '请选择规则类型' }],
colSpan: 12,
},
{
component: 'Select',
prop: 'match_field',
label: '匹配字段',
defaultValue: 'order_no',
componentProps: {
placeholder: '请选择匹配字段',
options: matchFieldOptions,
},
rules: [{ required: true, message: '请选择匹配字段' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'rule_pattern',
label: '正则表达式',
componentProps: {
placeholder: '请输入正则表达式,例如:^[A-Z0-9]{8,20}$',
},
rules: [{ required: true, message: '请输入正则表达式' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'step_ids',
label: '适用步骤',
defaultValue: [],
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
multiple: true,
clearable: true,
placeholder: '请选择适用步骤(不选则全部步骤)',
},
ifDetail: false,
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'is_enabled',
label: '是否启用',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: '排序',
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-specific-rule-page {
height: 100%;
}
</style>
@@ -0,0 +1,237 @@
<template>
<div class="produce-step-rule-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud, type CrudSchema } from '@/components/crud'
import { ProduceProcessStepService } from '@/apps/produce/api'
const normalizeArray = (value: unknown) => {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const formatPrerequisiteMode = (value: string) => {
const modes: Record<string, string> = {
all: '全部完成',
any: '任意完成',
none: '无需验证',
}
return modes[value] || value || '全部完成'
}
const formatYesNo = (value: unknown) => Number(value ?? 0) === 1 ? '是' : '否'
const formatPrerequisiteSteps = (row: any) => {
if (Array.isArray(row.prerequisite_steps_names) && row.prerequisite_steps_names.length > 0) {
return row.prerequisite_steps_names.join(', ')
}
const steps = normalizeArray(row.prerequisite_steps)
return steps.length > 0 ? `已配置 ${steps.length} 个步骤` : '无'
}
const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessStepService.list,
dialogTitle: '步骤规则',
editButtonText: '配置规则',
crudApi: {
edit: (params: any) => ProduceProcessStepService.update(params),
view: async (id: string | number) => {
const res = await ProduceProcessStepService.get(id)
return {
...res,
prerequisite_steps: normalizeArray(res?.prerequisite_steps),
prerequisite_mode: res?.prerequisite_mode || 'all',
prerequisite_require_final: Number(res?.prerequisite_require_final ?? 1),
allow_repeat_scan: Number(res?.allow_repeat_scan ?? 0),
repeat_scan_statuses: normalizeArray(res?.repeat_scan_statuses),
}
},
},
useCrud: true,
hasAdd: false,
hasEdit: true,
hasRemove: false,
hasView: false,
editAuth: 'produce:process-step:update',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'process_id',
label: '所属工序',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择所属工序',
},
colSpan: 8,
},
{
prop: 'LIKE_step_name',
label: '步骤名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入步骤名称',
},
colSpan: 8,
},
],
},
columns: [
{ type: 'index', label: '序号', width: 70 },
{ prop: 'process.process_name', label: '所属工序', minWidth: 160 },
{ prop: 'step_name', label: '步骤名称', minWidth: 160 },
{ prop: 'step_code', label: '步骤编码', minWidth: 130 },
{
prop: 'prerequisite_steps',
label: '前置必要步骤',
minWidth: 180,
formatter: formatPrerequisiteSteps,
},
{
prop: 'prerequisite_mode',
label: '前置验证模式',
minWidth: 130,
formatter: (row: any) => formatPrerequisiteMode(row.prerequisite_mode),
},
{
prop: 'prerequisite_require_final',
label: '要求终态',
width: 110,
formatter: (row: any) => formatYesNo(row.prerequisite_require_final ?? 1),
},
{
prop: 'allow_repeat_scan',
label: '允许重复扫码',
width: 130,
formatter: (row: any) => formatYesNo(row.allow_repeat_scan),
},
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '140px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, colSpan: 24 },
{ component: 'Input', prop: 'process_id', label: '工序ID', show: false, colSpan: 24 },
{ component: 'Input', prop: 'step_code', label: '步骤编码', show: false, colSpan: 24 },
{ component: 'Input', prop: 'sort', label: '排序', show: false, colSpan: 24 },
{ component: 'Input', prop: 'status', label: '状态', show: false, colSpan: 24 },
{
component: 'Input',
prop: 'step_name',
label: '步骤名称',
componentProps: {
disabled: true,
},
colSpan: 24,
},
{
component: 'ApiSelect',
prop: 'prerequisite_steps',
label: '前置必要步骤',
defaultValue: [],
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
multiple: true,
clearable: true,
placeholder: '请选择前置必要步骤',
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'prerequisite_mode',
label: '前置步骤验证模式',
defaultValue: 'all',
componentProps: {
options: [
{ label: '全部完成', value: 'all' },
{ label: '任意完成', value: 'any' },
{ label: '无需验证', value: 'none' },
],
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'prerequisite_require_final',
label: '前置步骤要求终态',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'allow_repeat_scan',
label: '允许重复扫码',
defaultValue: 0,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 24,
},
{
component: 'ApiSelect',
prop: 'repeat_scan_statuses',
label: '允许重复扫码的状态',
defaultValue: [],
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select', limit: 1000, status: 1 },
multiple: true,
clearable: true,
placeholder: '请选择允许重复扫码的状态',
},
colSpan: 24,
},
],
},
}
}
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-step-rule-page {
height: 100%;
}
</style>
@@ -0,0 +1,228 @@
<template>
<div class="exception-analysis-page">
<div class="analysis-toolbar">
<el-date-picker
v-model="filters.reported_at_range"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-input v-model="filters.type_code" clearable placeholder="处理类型编码" @keyup.enter="loadData" />
<el-select v-model="filters.status" clearable placeholder="处理状态">
<el-option label="待处理" :value="0" />
<el-option label="处理中" :value="1" />
<el-option label="已处理" :value="2" />
</el-select>
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="metric-grid">
<div class="metric-item"><span>异常总数</span><strong>{{ overview.total }}</strong></div>
<div class="metric-item pending"><span>待处理</span><strong>{{ overview.pending }}</strong></div>
<div class="metric-item processing"><span>处理中</span><strong>{{ overview.processing }}</strong></div>
<div class="metric-item completed"><span>已处理</span><strong>{{ overview.completed }}</strong></div>
</div>
<div class="analysis-grid" v-loading="loading">
<section class="analysis-section">
<h3>处理类型分布</h3>
<el-table :data="overview.by_type" size="small" height="260" empty-text="暂无数据">
<el-table-column prop="name" label="处理类型" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>异常原因排行</h3>
<el-table :data="overview.by_reason" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="异常原因" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>具体问题排行</h3>
<el-table :data="overview.by_issue" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="具体问题" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section full-width">
<h3>每日提报趋势</h3>
<el-table :data="overview.by_date" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="date" label="日期" min-width="180" />
<el-table-column prop="total" label="异常数量" width="130" align="right" />
</el-table>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import request from '@/utils/http'
interface AnalysisItem {
code: string
name: string
total: number
}
interface DateItem {
date: string
total: number
}
interface AnalysisOverview {
total: number
pending: number
processing: number
completed: number
by_type: AnalysisItem[]
by_reason: AnalysisItem[]
by_issue: AnalysisItem[]
by_date: DateItem[]
}
defineOptions({ name: 'ProduceExceptionAnalysis' })
const defaultRange = (): [string, string] => {
const today = new Date()
const start = new Date(today)
start.setDate(today.getDate() - 29)
const format = (date: Date) => date.toISOString().slice(0, 10)
return [format(start), format(today)]
}
const filters = reactive({
reported_at_range: defaultRange() as string[] | undefined,
type_code: '',
status: undefined as number | undefined,
})
const loading = ref(false)
const overview = ref<AnalysisOverview>({
total: 0,
pending: 0,
processing: 0,
completed: 0,
by_type: [],
by_reason: [],
by_issue: [],
by_date: [],
})
async function loadData() {
loading.value = true
try {
overview.value = await request.get<AnalysisOverview>({
url: '/produce/exception-analysis/overview',
params: filters,
})
} finally {
loading.value = false
}
}
function resetFilters() {
filters.reported_at_range = defaultRange()
filters.type_code = ''
filters.status = undefined
loadData()
}
onMounted(loadData)
</script>
<style scoped>
.exception-analysis-page {
min-height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.analysis-toolbar,
.metric-grid,
.analysis-grid {
display: grid;
gap: 14px;
}
.analysis-toolbar {
grid-template-columns: minmax(260px, 1fr) 180px 150px auto auto;
align-items: center;
padding: 14px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 16px 0;
}
.metric-item,
.analysis-section {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-item {
padding: 18px;
}
.metric-item span,
.metric-item strong {
display: block;
}
.metric-item span {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.metric-item strong {
margin-top: 8px;
color: var(--el-text-color-primary);
font-size: 28px;
line-height: 1;
}
.metric-item.pending strong { color: var(--el-color-danger); }
.metric-item.processing strong { color: var(--el-color-warning); }
.metric-item.completed strong { color: var(--el-color-success); }
.analysis-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.analysis-section {
min-width: 0;
padding: 14px;
}
.analysis-section h3 {
margin: 0 0 12px;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.full-width {
grid-column: 1 / -1;
}
@media (max-width: 1100px) {
.analysis-toolbar,
.analysis-grid { grid-template-columns: 1fr 1fr; }
.metric-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
.analysis-toolbar,
.analysis-grid,
.metric-grid { grid-template-columns: 1fr; }
}
</style>
@@ -0,0 +1,22 @@
<template>
<div class="produce-exception-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionDataList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-exception-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,127 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionRecordService } from '@/apps/produce/api'
const statusOptions = [
{ label: '待处理', value: 0 },
{ label: '处理中', value: 1 },
{ label: '已处理', value: 2 },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionRecordService.list,
dialogTitle: '异常数据',
crudApi: {
add: ProduceExceptionRecordService.create,
edit: ProduceExceptionRecordService.update,
remove: ProduceExceptionRecordService.remove,
view: ProduceExceptionRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-data:create',
editAuth: 'produce:exception-data:update',
removeAuth: 'produce:exception-data:delete',
viewAuth: 'produce:exception-data:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' },
colSpan: 8,
},
{
prop: 'type_code',
label: '处理类型',
component: 'Input',
componentProps: { clearable: true, placeholder: '请输入处理类型编码' },
colSpan: 8,
},
{
prop: 'status',
label: '处理状态',
component: 'Select',
componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions },
colSpan: 8,
},
{
prop: 'reported_at_range',
label: '提报时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'order_no', label: '单号/条码', minWidth: 160 },
{ prop: 'type_name', label: '处理类型', minWidth: 120 },
{ prop: 'reason_name', label: '异常原因', minWidth: 120 },
{ prop: 'issue_name', label: '具体问题', minWidth: 140 },
{ prop: 'reporter', label: '提报人', minWidth: 110 },
{ prop: 'status_name', label: '处理状态', minWidth: 100 },
{ prop: 'handler', label: '处理人', minWidth: 110 },
{ prop: 'reported_date', label: '提报时间', minWidth: 170 },
{ prop: 'handled_date', label: '处理时间', minWidth: 170 },
],
dialogProps: { width: '820px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Input', prop: 'order_no', label: '单号/条码',
componentProps: { placeholder: '请输入单号或条码', maxlength: 100 },
rules: [{ required: true, message: '请输入单号或条码' }], colSpan: 12,
},
{ component: 'Input', prop: 'user_code', label: '提报人工号', componentProps: { placeholder: '请输入提报人工号', maxlength: 50 }, colSpan: 12 },
{ component: 'Input', prop: 'reporter', label: '提报人', componentProps: { placeholder: '请输入提报人', maxlength: 100 }, colSpan: 12 },
{
component: 'Input', prop: 'type_code', label: '处理类型编码',
componentProps: { placeholder: '例如 defective_repair', maxlength: 64 },
rules: [{ required: true, message: '请输入处理类型编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'type_name', label: '处理类型',
componentProps: { placeholder: '例如 不良维修', maxlength: 100 },
rules: [{ required: true, message: '请输入处理类型' }], colSpan: 12,
},
{
component: 'Input', prop: 'reason_code', label: '异常原因编码',
componentProps: { placeholder: '例如 test_defect', maxlength: 64 },
rules: [{ required: true, message: '请输入异常原因编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'reason_name', label: '异常原因',
componentProps: { placeholder: '例如 测试不良', maxlength: 100 },
rules: [{ required: true, message: '请输入异常原因' }], colSpan: 12,
},
{ component: 'Input', prop: 'issue_code', label: '具体问题编码', componentProps: { placeholder: '可选', maxlength: 64 }, colSpan: 12 },
{ component: 'Input', prop: 'issue_name', label: '具体问题', componentProps: { placeholder: '可选', maxlength: 100 }, colSpan: 12 },
{ component: 'Select', prop: 'status', label: '处理状态', defaultValue: 0, componentProps: { options: statusOptions }, colSpan: 12 },
{ component: 'Input', prop: 'handler', label: '处理人', componentProps: { placeholder: '请输入处理人', maxlength: 100 }, colSpan: 12 },
{
component: 'Upload', prop: 'images', label: '现场图片', componentProps: { listType: 'picture-card' }, ifDetail: true, colSpan: 24,
},
{
component: 'Input', prop: 'remark', label: '补充说明',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,22 @@
<template>
<div class="exception-setting-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionSettingList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.exception-setting-page {
height: 100%;
}
</style>
@@ -0,0 +1,100 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionSettingService } from '@/apps/produce/api'
const settingTypeOptions = [
{ label: '处理类型', value: 'type' },
{ label: '异常原因', value: 'reason' },
{ label: '具体问题', value: 'issue' },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionSettingService.list,
dialogTitle: '异常设置',
crudApi: {
add: ProduceExceptionSettingService.create,
edit: ProduceExceptionSettingService.update,
remove: ProduceExceptionSettingService.remove,
view: ProduceExceptionSettingService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-setting:create',
editAuth: 'produce:exception-setting:update',
removeAuth: 'produce:exception-setting:delete',
viewAuth: 'produce:exception-setting:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword', label: '关键字', component: 'Input',
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
},
{
prop: 'setting_type', label: '设置层级', component: 'Select',
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
},
{
prop: 'is_enabled', label: '启用状态', component: 'Select',
componentProps: {
clearable: true, placeholder: '请选择启用状态',
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
}, colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
{ prop: 'code', label: '编码', minWidth: 150 },
{ prop: 'name', label: '名称', minWidth: 160 },
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
{ prop: 'sort', label: '排序', width: 90 },
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ prop: 'created_name', label: '创建人', minWidth: 110 },
],
dialogProps: { width: '700px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Select', prop: 'setting_type', label: '设置层级',
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
},
{
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
componentProps: {
api: '/produce/exception-setting', requestMethod: 'get',
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
}, colSpan: 12,
},
{
component: 'Input', prop: 'code', label: '设置编码',
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'name', label: '设置名称',
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
},
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
{
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
},
{
component: 'Input', prop: 'remark', label: '备注',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,24 @@
<template>
<div class="produce-material-category-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceMaterialCategoryList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-material-category-page { height: 100%; }
</style>
@@ -0,0 +1,42 @@
import { CrudSchema } from '@/components/crud'
import { ProduceMaterialCategoryService } from '../../../../api'
export const crudSchema = (): CrudSchema => ({
api: ProduceMaterialCategoryService.list,
dialogTitle: '物料分类',
crudApi: {
add: ProduceMaterialCategoryService.create,
edit: ProduceMaterialCategoryService.update,
remove: ProduceMaterialCategoryService.remove,
view: ProduceMaterialCategoryService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:material-category:create',
editAuth: 'produce:material-category:update',
removeAuth: 'produce:material-category:delete',
viewAuth: 'produce:material-category:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [{ prop: 'LIKE_category_name', label: '分类名称', component: 'Input', componentProps: { clearable: true, placeholder: '请输入分类名称' }, colSpan: 8 }],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'category_name', label: '分类名称', minWidth: 220 },
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
],
dialogProps: { width: '520px' },
formSchema: {
showDefaultActions: false,
labelWidth: '90px',
schema: [
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
{ component: 'Input', prop: 'category_name', label: '分类名称', componentProps: { maxlength: 100, placeholder: '请输入分类名称' }, rules: [{ required: true, message: '请输入分类名称' }], colSpan: 24 },
],
},
})
@@ -0,0 +1,49 @@
<template>
<div class="produce-material-page art-full-height">
<div class="material-actions">
<ElUpload :show-file-list="false" accept=".xlsx,.xls,.csv" :http-request="importMaterials">
<ElButton type="primary" plain>Excel 导入</ElButton>
</ElUpload>
<span class="material-hint">表头:分类、商品编码、商品名称;同编码将更新。</span>
</div>
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { ElMessage } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import { useCrud } from '@/components/crud'
import { ProduceMaterialService } from '../../../api'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceMaterialList' })
const [BasicCrud, crudExpose] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
async function importMaterials(options: UploadRequestOptions) {
try {
const formData = new FormData()
formData.append('file', options.file)
const result = await ProduceMaterialService.importExcel(formData)
ElMessage.success(`导入成功:新增 ${result.created} 条,更新 ${result.updated} 条`)
options.onSuccess?.(result)
crudExpose.refreshCreate()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : 'Excel导入失败')
options.onError?.(error as Error)
}
}
</script>
<style scoped>
.produce-material-page { height: 100%; }
.material-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.material-hint { color: var(--el-text-color-secondary); font-size: 13px; }
</style>
@@ -0,0 +1,50 @@
import { CrudSchema } from '@/components/crud'
import { ProduceMaterialService } from '../../../../api'
export const crudSchema = (): CrudSchema => ({
api: ProduceMaterialService.list,
dialogTitle: '物料库',
crudApi: {
add: ProduceMaterialService.create,
edit: ProduceMaterialService.update,
remove: ProduceMaterialService.remove,
view: ProduceMaterialService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
hasExport: true,
addAuth: 'produce:material:create',
editAuth: 'produce:material:update',
removeAuth: 'produce:material:delete',
viewAuth: 'produce:material:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{ prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 },
{ prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, colSpan: 8 },
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'category_name', label: '物料分类', minWidth: 160 },
{ prop: 'product_code', label: '商品编码', minWidth: 180 },
{ prop: 'product_name', label: '商品名称', minWidth: 220 },
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
],
dialogProps: { width: '640px' },
formSchema: {
showDefaultActions: false,
labelWidth: '90px',
schema: [
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
{ component: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, rules: [{ required: true, message: '请选择物料分类' }], colSpan: 12 },
{ component: 'Input', prop: 'product_code', label: '商品编码', componentProps: { maxlength: 100, placeholder: '请输入或扫描商品编码' }, rules: [{ required: true, message: '请输入商品编码' }], colSpan: 12 },
{ component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 },
],
},
})
@@ -0,0 +1,26 @@
<template>
<div class="produce-process-step-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProcessStepList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-process-step-page {
height: 100%;
}
</style>
@@ -0,0 +1,185 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceProcessStepService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessStepService.list,
dialogTitle: $t('produce.modules.process_step.title'),
crudApi: {
add: ProduceProcessStepService.create,
edit: ProduceProcessStepService.update,
remove: ProduceProcessStepService.remove,
view: ProduceProcessStepService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:process-step:create',
editAuth: 'produce:process-step:update',
removeAuth: 'produce:process-step:delete',
viewAuth: 'produce:process-step:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'process_id',
label: $t('produce.modules.process_step.search.process_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.process_step.search.process_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_step_code',
label: $t('produce.modules.process_step.search.step_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process_step.search.step_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_step_name',
label: $t('produce.modules.process_step.search.step_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process_step.search.step_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.process_step.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.process_step.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{
prop: 'process.process_name',
label: $t('produce.modules.process_step.columns.process_name'),
minWidth: 180,
},
{ prop: 'step_code', label: $t('produce.modules.process_step.columns.step_code'), minWidth: 160 },
{ prop: 'step_name', label: $t('produce.modules.process_step.columns.step_name'), minWidth: 180 },
{ prop: 'sort', label: $t('produce.modules.process_step.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.process_step.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.process_step.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '680px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'process_id',
label: $t('produce.modules.process_step.form.process_id'),
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.process_step.form.process_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.process_id_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'step_code',
label: $t('produce.modules.process_step.columns.step_code'),
componentProps: {
placeholder: $t('produce.modules.process_step.form.step_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.step_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'step_name',
label: $t('produce.modules.process_step.columns.step_name'),
componentProps: {
placeholder: $t('produce.modules.process_step.form.step_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.step_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.process_step.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.process_step.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.process_step.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.process_step.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-process-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProcessList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-process-page {
height: 100%;
}
</style>
@@ -0,0 +1,154 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceProcessService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessService.list,
dialogTitle: $t('produce.modules.process.title'),
crudApi: {
add: ProduceProcessService.create,
edit: ProduceProcessService.update,
remove: ProduceProcessService.remove,
view: ProduceProcessService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:process:create',
editAuth: 'produce:process:update',
removeAuth: 'produce:process:delete',
viewAuth: 'produce:process:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_process_code',
label: $t('produce.modules.process.search.process_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process.search.process_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_process_name',
label: $t('produce.modules.process.search.process_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process.search.process_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.process.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.process.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'process_code', label: $t('produce.modules.process.columns.process_code'), minWidth: 160 },
{ prop: 'process_name', label: $t('produce.modules.process.columns.process_name'), minWidth: 180 },
{ prop: 'step_count', label: $t('produce.modules.process.columns.step_count'), width: 120 },
{ prop: 'sort', label: $t('produce.modules.process.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.process.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.process.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '640px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'process_code',
label: $t('produce.modules.process.columns.process_code'),
componentProps: {
placeholder: $t('produce.modules.process.form.process_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.process.form.process_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'process_name',
label: $t('produce.modules.process.columns.process_name'),
componentProps: {
placeholder: $t('produce.modules.process.form.process_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.process.form.process_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.process.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.process.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.process.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.process.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,22 @@
<template>
<div class="produce-product-record-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProductRecordList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-product-record-page {
height: 100%;
}
</style>
@@ -0,0 +1,249 @@
import { CrudSchema } from '@/components/crud'
import { ProduceProductRecordService } from '@/apps/produce/api'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProductRecordService.list,
dialogTitle: '整机数据',
crudApi: {
add: ProduceProductRecordService.create,
edit: ProduceProductRecordService.update,
remove: ProduceProductRecordService.remove,
view: ProduceProductRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:product-record:create',
editAuth: 'produce:product-record:update',
removeAuth: 'produce:product-record:delete',
viewAuth: 'produce:product-record:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入加工单号/商品编码/操作人员/备注',
},
colSpan: 6,
},
{
prop: 'process_id',
label: '工序名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
},
colSpan: 6,
},
{
prop: 'step_id',
label: '步骤名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
},
colSpan: 6,
},
{
prop: 'status_id',
label: '步骤状态',
component: 'ApiSelect',
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
},
colSpan: 6,
},
{
prop: 'scan_result',
label: '扫码结果',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择扫码结果',
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
colSpan: 6,
},
{
prop: 'created_time_range',
label: '创建时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'process_order_no', label: '加工单号', minWidth: 160 },
{ prop: 'product_code', label: '商品编码', minWidth: 160 },
{ prop: 'quantity', label: '数量', width: 100 },
{ prop: 'process_name', label: '工序名称', minWidth: 140 },
{ prop: 'step_name', label: '步骤名称', minWidth: 140 },
{ prop: 'status_name', label: '步骤状态', minWidth: 120 },
{ prop: 'operator', label: '操作人员', minWidth: 120 },
{
prop: 'scan_result',
label: '扫码结果',
width: 100,
formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'),
},
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
{ prop: 'created_name', label: '创建人', minWidth: 120 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'process_order_no',
label: '加工单号',
componentProps: {
placeholder: '请输入加工单号',
maxlength: 100,
},
rules: [{ required: true, message: '请输入加工单号' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'product_code',
label: '商品编码',
componentProps: {
placeholder: '请输入商品编码',
maxlength: 100,
},
rules: [{ required: true, message: '请输入商品编码' }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'quantity',
label: '数量',
defaultValue: 1,
componentProps: {
min: 1,
precision: 0,
},
rules: [{ required: true, message: '请输入数量' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'user_code',
label: '用户编码',
componentProps: {
placeholder: '请输入用户编码',
maxlength: 50,
},
rules: [{ required: true, message: '请输入用户编码' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'operator',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员',
maxlength: 100,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'process_name',
label: '工序名称',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'step_name',
label: '步骤名称',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'status_name',
label: '步骤状态',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'scan_result',
label: '扫码结果',
defaultValue: 1,
componentProps: {
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
rules: [{ required: true, message: '请选择扫码结果' }],
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,49 @@
<template>
<div class="produce-hardware-fault-description-page art-full-height">
<div class="hardware-fault-actions">
<ElUpload :show-file-list="false" accept=".xlsx,.xls,.csv" :http-request="importHardwareFaultDescriptions">
<ElButton type="primary" plain>Excel 导入</ElButton>
</ElUpload>
<span class="hardware-fault-hint">表头:分类、描述;同分类同描述将自动跳过并计为更新。</span>
</div>
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { ElMessage } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import { useCrud } from '@/components/crud'
import { ProduceHardwareFaultDescriptionService } from '@/apps/produce/api'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceHardwareFaultDescriptionList' })
const [BasicCrud, crudExpose] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
async function importHardwareFaultDescriptions(options: UploadRequestOptions) {
try {
const formData = new FormData()
formData.append('file', options.file)
const result = await ProduceHardwareFaultDescriptionService.importExcel(formData)
ElMessage.success(`导入成功:新增 ${result.created} 条,更新 ${result.updated} 条`)
options.onSuccess?.(result)
crudExpose.refreshCreate()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : 'Excel导入失败')
options.onError?.(error as Error)
}
}
</script>
<style scoped>
.produce-hardware-fault-description-page { height: 100%; }
.hardware-fault-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.hardware-fault-hint { color: var(--el-text-color-secondary); font-size: 13px; }
</style>
@@ -0,0 +1,135 @@
import { CrudSchema } from "@/components/crud";
import { ProduceHardwareFaultDescriptionService } from "@/apps/produce/api";
const enabledOptions = [
{ label: "启用", value: 1 },
{ label: "停用", value: 0 },
];
export const crudSchema = (): CrudSchema => ({
api: ProduceHardwareFaultDescriptionService.list,
dialogTitle: "硬件故障描述",
crudApi: {
add: ProduceHardwareFaultDescriptionService.create,
edit: ProduceHardwareFaultDescriptionService.update,
remove: ProduceHardwareFaultDescriptionService.remove,
view: ProduceHardwareFaultDescriptionService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: "produce:hardware-fault-description:create",
editAuth: "produce:hardware-fault-description:update",
removeAuth: "produce:hardware-fault-description:delete",
viewAuth: "produce:hardware-fault-description:read",
rowKey: "id",
searchFormSchema: {
labelWidth: "90px",
schema: [
{
prop: "category_id",
label: "物料分类",
component: "ApiSelect",
componentProps: {
api: ProduceHardwareFaultDescriptionService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
},
colSpan: 8,
},
{
prop: "keyword",
label: "关键字",
component: "Input",
componentProps: { clearable: true, placeholder: "故障描述或备注" },
colSpan: 8,
},
{
prop: "is_enabled",
label: "启用状态",
component: "Select",
componentProps: { clearable: true, options: enabledOptions },
colSpan: 8,
},
],
},
columns: [
{ type: "selection" },
{ type: "index", label: "序号", width: 70 },
{ prop: "category_name", label: "物料分类", minWidth: 160 },
{
prop: "description",
label: "硬件故障描述",
minWidth: 300,
showOverflowTooltip: true,
},
{ prop: "use_count", label: "使用次数", width: 100 },
{
prop: "is_enabled",
label: "状态",
width: 90,
formatter: (row: { is_enabled?: number }) =>
row.is_enabled === 0 ? "停用" : "启用",
},
{ prop: "remark", label: "备注", minWidth: 180, showOverflowTooltip: true },
{ prop: "created_date", label: "创建时间", minWidth: 180 },
],
dialogProps: { width: "720px" },
formSchema: {
showDefaultActions: false,
labelWidth: "110px",
schema: [
{
component: "Input",
prop: "id",
label: "ID",
show: false,
ifDetail: true,
colSpan: 24,
},
{
component: "ApiSelect",
prop: "category_id",
label: "物料分类",
componentProps: {
api: ProduceHardwareFaultDescriptionService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
placeholder: "请选择物料分类",
},
rules: [{ required: true, message: "请选择物料分类" }],
colSpan: 12,
},
{
component: "RadioGroup",
prop: "is_enabled",
label: "是否启用",
defaultValue: 1,
componentProps: { options: enabledOptions },
colSpan: 12,
},
{
component: "Input",
prop: "description",
label: "故障描述",
componentProps: {
maxlength: 500,
placeholder: "请输入该物料分类的硬件故障描述",
},
rules: [{ required: true, message: "请输入硬件故障描述" }],
colSpan: 24,
},
{
component: "Input",
prop: "remark",
label: "备注",
componentProps: { type: "textarea", rows: 3, maxlength: 500 },
colSpan: 24,
},
],
},
});
@@ -0,0 +1,22 @@
<template>
<div class="produce-repair-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
defineOptions({ name: "ProduceRepairDataList" });
const [BasicCrud] = useCrud({
...crudSchema(),
});
</script>
<style scoped>
.produce-repair-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,490 @@
import { h } from "vue";
import { CrudSchema } from "@/components/crud";
import { ProduceRepairRecordService } from "@/apps/produce/api";
const formatPartSource = (source: unknown): string => {
const labels: Record<string, string> = {
new: "新料",
old: "售后",
after_sale: "售后",
after_sales: "售后",
"after-sale": "售后",
repair: "返修",
refurbished: "返修",
};
const value = String(source || "");
return labels[value] || value || "-";
};
const parseParts = (parts: Array<Record<string, any>> | string | null | undefined): Array<Record<string, any>> => {
if (Array.isArray(parts)) return parts;
if (typeof parts !== "string" || !parts) return [];
try {
const parsed = JSON.parse(parts);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const formatPartValues = (
parts: Array<Record<string, any>> | string | null | undefined,
getValue: (part: Record<string, any>) => string,
): string =>
parseParts(parts)
.map((part) => getValue(part))
.filter(Boolean)
.join("\n");
const formatPartRows = (
parts: Array<Record<string, any>> | string | null | undefined,
getValue: (part: Record<string, any>) => string,
nowrap = true,
) =>
h(
"div",
parseParts(parts).map((part) =>
h(
"div",
{ style: { lineHeight: "24px", minHeight: "24px", whiteSpace: nowrap ? "nowrap" : "normal" } },
getValue(part) || "-",
),
),
);
const formatPartFaultDescriptions = (part: Record<string, any>): string =>
Array.isArray(part.fault_descriptions)
? part.fault_descriptions
.map((fault: { description?: string }) => fault.description || "")
.filter(Boolean)
.join("、")
: "";
const formatCategoryNames = (row: {
category_names?: string[] | string | null;
category_name?: string | null;
}): string => {
const names = row.category_names;
if (Array.isArray(names)) return names.filter(Boolean).join("、");
if (typeof names === "string" && names) {
try {
const parsed = JSON.parse(names);
if (Array.isArray(parsed)) return parsed.filter(Boolean).join("、");
} catch {
return names;
}
return names;
}
return row.category_name || "";
};
type ResponsibilitySnapshot = {
process_name?: string;
step_name?: string;
operator?: string;
user_code?: string;
};
const parseSnapshots = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): ResponsibilitySnapshot[] => {
if (Array.isArray(snapshots)) return snapshots;
if (typeof snapshots !== "string" || !snapshots) return [];
try {
const parsed = JSON.parse(snapshots);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const formatRepairResult = (repairResult?: string | null): string => {
const labels: Record<string, string> = {
material_defect: "物料不良",
operation_defect: "作业不良",
assembly_mismatch: "搭配错误",
};
return labels[repairResult || ""] || repairResult || "";
};
const formatOperationFaultSteps = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => [item.process_name, item.step_name].filter(Boolean).join(" · "))
.filter(Boolean)
.join("\n");
const formatResponsibleOperators = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => {
const operator = item.operator || item.user_code || "";
const process = [item.process_name, item.step_name].filter(Boolean).join(" · ");
return [operator, process].filter(Boolean).join("|");
})
.filter(Boolean)
.join("\n");
const formatResponsibleOperatorNames = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => {
const step = item.step_name || "";
const operator = item.operator || item.user_code || "";
return [step, operator].filter(Boolean).join("-");
})
.filter(Boolean)
.join("\n");
export const crudSchema = (): CrudSchema => ({
api: ProduceRepairRecordService.list,
dialogTitle: "维修数据",
crudApi: {
add: ProduceRepairRecordService.create,
edit: ProduceRepairRecordService.update,
remove: ProduceRepairRecordService.remove,
view: ProduceRepairRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
hasExport: true,
exportFileName: "维修数据",
exportFields: [
{ field: "repair_no", title: "维修单号" },
{ field: "order_product", title: "订单号/商品编码" },
{ field: "exception_type_name", title: "异常分类" },
{ field: "exception_reason", title: "异常原因" },
{ field: "part_category", title: "物料分类" },
{ field: "part_code", title: "物料编码" },
{ field: "part_name", title: "更换备件" },
{ field: "part_fault", title: "故障描述" },
{ field: "part_quantity", title: "数量" },
{ field: "part_source", title: "来源" },
{ field: "repair_result_name", title: "维修判定分类" },
{ field: "responsible_operator_names", title: "责任人员" },
{ field: "operator", title: "维修人员" },
{ field: "repaired_date", title: "维修时间" },
],
addAuth: "produce:repair-record:create",
editAuth: "produce:repair-record:update",
removeAuth: "produce:repair-record:delete",
viewAuth: "produce:repair-record:read",
rowKey: "id",
searchFormSchema: {
labelWidth: "90px",
schema: [
{
prop: "keyword",
label: "关键字",
component: "Input",
componentProps: {
clearable: true,
placeholder: "维修单、订单、商品或故障描述",
},
colSpan: 8,
},
{
prop: "category_id",
label: "物料分类",
component: "ApiSelect",
componentProps: {
api: ProduceRepairRecordService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
},
colSpan: 8,
},
{
prop: "fault_description_id",
label: "故障描述",
component: "ApiSelect",
componentProps: {
api: ProduceRepairRecordService.faultDescriptionOptions,
labelField: "description",
valueField: "id",
clearable: true,
},
colSpan: 8,
},
],
},
columns: [
{ type: "selection" },
{ type: "index", label: "序号", width: 70 },
{ prop: "repair_no", label: "维修单号", minWidth: 160 },
{
prop: "order_no",
label: "订单号/商品编码",
minWidth: 180,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { order_no?: string | null; product_code?: string | null }) =>
[row.order_no, row.product_code].filter(Boolean).join("/"),
},
{ prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } },
{
prop: "part_category",
label: "物料分类",
minWidth: 140,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.category_name || ""),
},
{
prop: "part_code",
label: "物料编码",
minWidth: 150,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.product_code || ""),
},
{
prop: "part_name",
label: "更换备件",
minWidth: 240,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.product_name || "", true),
},
{
prop: "part_fault",
label: "故障描述",
minWidth: 180,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, formatPartFaultDescriptions),
},
{
prop: "part_quantity",
label: "数量",
width: 80,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => String(part.quantity ?? 1)),
},
{
prop: "part_source",
label: "来源",
width: 90,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => formatPartSource(part.source)),
},
{
prop: "repair_result",
label: "维修判定分类",
minWidth: 140,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { repair_result?: string | null }) => formatRepairResult(row.repair_result),
},
{
prop: "responsible_operators",
label: "责任人员",
minWidth: 120,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) =>
formatResponsibleOperatorNames(row.responsible_operators),
},
{ prop: "operator", label: "维修人员", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "repaired_date", label: "维修时间", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } },
],
dialogProps: { width: "860px" },
formSchema: {
showDefaultActions: false,
labelWidth: "105px",
schema: [
{
component: "Input",
prop: "id",
label: "ID",
show: false,
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "repair_no",
label: "维修单号",
componentProps: { maxlength: 100, placeholder: "请输入维修单号" },
rules: [{ required: true, message: "请输入维修单号" }],
colSpan: 12,
},
{
component: "Input",
prop: "order_no",
label: "订单号",
componentProps: { maxlength: 100, placeholder: "请输入订单号" },
colSpan: 12,
},
{
component: "Input",
prop: "product_code",
label: "商品编码",
componentProps: { maxlength: 100, placeholder: "请输入商品编码" },
colSpan: 12,
},
{
component: "Input",
prop: "product_name",
label: "商品名称",
componentProps: { maxlength: 200, placeholder: "请输入商品名称" },
colSpan: 12,
},
{
component: "ApiSelect",
prop: "category_id",
label: "首选物料分类",
componentProps: {
api: ProduceRepairRecordService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
placeholder: "请选择首选物料分类",
},
colSpan: 12,
},
{
component: "Input",
prop: "category_names",
label: "物料分类",
componentProps: (model: { category_names?: string[] | string | null; category_name?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatCategoryNames(model),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "ApiSelect",
prop: "fault_description_id",
label: "故障描述",
componentProps: {
api: ProduceRepairRecordService.faultDescriptionOptions,
labelField: "description",
valueField: "id",
clearable: true,
placeholder: "请选择硬件故障描述",
},
colSpan: 12,
},
{
component: "Input",
prop: "exception_reason",
label: "异常原因",
componentProps: (model: { exception_reason?: string | null; exception_issue?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: [model.exception_reason, model.exception_issue].filter(Boolean).join(" · "),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "fault_descriptions",
label: "故障描述",
componentProps: (model: { fault_descriptions?: string[]; fault_description?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: (model.fault_descriptions || []).join("、") || model.fault_description || "",
}),
ifDetail: true,
colSpan: 24,
},
{
component: "PartsDetail",
prop: "parts",
label: "更换备件",
ifDetail: true,
colSpan: 24,
},
{
component: "Select",
prop: "repair_result",
label: "维修判定",
componentProps: {
clearable: true,
options: [
{ label: "物料不良", value: "material_defect" },
{ label: "作业不良", value: "operation_defect" },
{ label: "搭配错误", value: "assembly_mismatch" },
],
},
colSpan: 12,
},
{
component: "Input",
prop: "repair_result_detail",
label: "维修判定",
componentProps: (model: { repair_result?: string | null }) => ({
readonly: true,
value: formatRepairResult(model.repair_result),
}),
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "operation_fault_steps",
label: "责任步骤",
componentProps: (model: { operation_fault_steps?: ResponsibilitySnapshot[] | string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatOperationFaultSteps(model.operation_fault_steps),
}),
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "responsible_operators",
label: "责任人员",
componentProps: (model: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatResponsibleOperators(model.responsible_operators),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "Upload",
prop: "images",
label: "维修现场图片",
componentProps: { listType: "picture-card" },
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "operator",
label: "维修人员",
componentProps: { maxlength: 100, placeholder: "请输入维修人员" },
colSpan: 12,
},
{
component: "DatePicker",
prop: "repaired_at",
label: "维修时间",
componentProps: {
type: "datetime",
valueFormat: "x",
placeholder: "请选择维修时间",
},
colSpan: 12,
},
{
component: "Input",
prop: "remark",
label: "备注",
componentProps: { type: "textarea", rows: 3, maxlength: 1000 },
colSpan: 24,
},
],
},
});
@@ -0,0 +1,22 @@
<template>
<div class="repair-setting-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceRepairSettingList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.repair-setting-page {
height: 100%;
}
</style>
@@ -0,0 +1,100 @@
import { CrudSchema } from '@/components/crud'
import { ProduceRepairSettingService } from '@/apps/produce/api'
const settingTypeOptions = [
{ label: '处理类型', value: 'type' },
{ label: '维修原因', value: 'reason' },
{ label: '具体问题', value: 'issue' },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceRepairSettingService.list,
dialogTitle: '维修设置',
crudApi: {
add: ProduceRepairSettingService.create,
edit: ProduceRepairSettingService.update,
remove: ProduceRepairSettingService.remove,
view: ProduceRepairSettingService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:repair-setting:create',
editAuth: 'produce:repair-setting:update',
removeAuth: 'produce:repair-setting:delete',
viewAuth: 'produce:repair-setting:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword', label: '关键字', component: 'Input',
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
},
{
prop: 'setting_type', label: '设置层级', component: 'Select',
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
},
{
prop: 'is_enabled', label: '启用状态', component: 'Select',
componentProps: {
clearable: true, placeholder: '请选择启用状态',
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
}, colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
{ prop: 'code', label: '编码', minWidth: 150 },
{ prop: 'name', label: '名称', minWidth: 160 },
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
{ prop: 'sort', label: '排序', width: 90 },
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ prop: 'created_name', label: '创建人', minWidth: 110 },
],
dialogProps: { width: '700px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Select', prop: 'setting_type', label: '设置层级',
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
},
{
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
componentProps: {
api: '/produce/repair-setting', requestMethod: 'get',
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
}, colSpan: 12,
},
{
component: 'Input', prop: 'code', label: '设置编码',
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'name', label: '设置名称',
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
},
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
{
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
},
{
component: 'Input', prop: 'remark', label: '备注',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,22 @@
<template>
<div class="produce-scan-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceScanDataList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-scan-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,254 @@
import { CrudSchema } from '@/components/crud'
import { ProduceScanDataService } from '@/apps/produce/api'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceScanDataService.list,
dialogTitle: '扫码数据',
crudApi: {
add: ProduceScanDataService.create,
edit: ProduceScanDataService.update,
remove: ProduceScanDataService.remove,
view: ProduceScanDataService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:scan-data:create',
editAuth: 'produce:scan-data:update',
removeAuth: 'produce:scan-data:delete',
viewAuth: 'produce:scan-data:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入订单号/用户编码/操作人员/备注',
},
colSpan: 6,
},
{
prop: 'process_id',
label: '工序名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
},
colSpan: 6,
},
{
prop: 'step_id',
label: '步骤名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
},
colSpan: 6,
},
{
prop: 'status_id',
label: '步骤状态',
component: 'ApiSelect',
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
},
colSpan: 6,
},
{
prop: 'LIKE_operator',
label: '操作人员',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入操作人员',
},
colSpan: 6,
},
{
prop: 'scan_result',
label: '扫码结果',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择扫码结果',
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
colSpan: 6,
},
{
prop: 'scan_time_range',
label: '扫码时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'order_no', label: '订单号', minWidth: 160 },
{ prop: 'quantity', label: '数量', width: 90 },
{ prop: 'user_code', label: '用户编码', minWidth: 120 },
{ prop: 'process_name', label: '工序名称', minWidth: 140 },
{ prop: 'step_name', label: '步骤名称', minWidth: 140 },
{ prop: 'status_name', label: '步骤状态', minWidth: 120 },
{ prop: 'operator', label: '操作人员', minWidth: 120 },
{
prop: 'scan_result',
label: '扫码结果',
width: 100,
formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'),
},
{ prop: 'scan_date', label: '扫码时间', minWidth: 180 },
{ prop: 'created_name', label: '创建人', minWidth: 120 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'order_no',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
maxlength: 100,
},
rules: [{ required: true, message: '请输入订单号' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'user_code',
label: '用户编码',
componentProps: {
placeholder: '请输入用户编码',
maxlength: 50,
},
rules: [{ required: true, message: '请输入用户编码' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'operator',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员',
maxlength: 100,
},
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'process_id',
label: '工序名称',
componentProps: (values: any) => ({
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择工序' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'step_id',
label: '步骤名称',
componentProps: (values: any) => ({
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择步骤' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'status_id',
label: '步骤状态',
componentProps: (values: any) => ({
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择状态' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'scan_result',
label: '扫码结果',
defaultValue: 1,
componentProps: {
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
rules: [{ required: true, message: '请选择扫码结果' }],
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,310 @@
<template>
<div class="statistics-page art-full-height">
<div class="toolbar">
<el-date-picker
v-model="date"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
@change="loadData"
/>
<el-select v-model="filters.data_type" placeholder="数据类型" @change="loadData">
<el-option label="全部数据" value="all" />
<el-option label="订单数据" value="order" />
<el-option label="整机数据" value="machine" />
</el-select>
<el-input
v-model="filters.user_code"
clearable
placeholder="用户编码"
@keyup.enter="loadData"
/>
<el-input
v-model="filters.operator"
clearable
placeholder="操作人员"
@keyup.enter="loadData"
/>
<el-select
v-model="filters.process_id"
clearable
filterable
placeholder="选择工序名称"
@change="handleProcessChange"
>
<el-option
v-for="option in processOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-select
v-model="filters.step_id"
clearable
filterable
placeholder="选择步骤名称"
>
<el-option
v-for="option in stepOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-button type="primary" :loading="loading" @click="loadData"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
<el-button :disabled="!items.length" @click="exportData">导出</el-button>
</div>
<div class="table-wrapper" v-loading="loading">
<div class="table-scroll">
<el-table
:data="items"
border
stripe
:style="{ width: tableWidth }"
empty-text="暂无统计数据"
:span-method="spanMethod"
:row-class-name="rowClassName"
>
<el-table-column type="index" label="序号" width="65" fixed="left" />
<el-table-column
prop="user_code"
label="工位号"
width="110"
fixed="left"
/>
<el-table-column
prop="operator"
label="操作人员"
width="120"
fixed="left"
/>
<el-table-column prop="production_process" label="生产工序" width="220" fixed="left" />
<el-table-column prop="total" label="总数" width="75" fixed="left" />
<el-table-column
v-for="column in columns"
:key="column.key"
:prop="column.key"
:label="column.label"
width="64"
align="center"
/>
</el-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import {
ProduceWorkStatisticsService,
type WorkStatisticsColumn,
type WorkStatisticsOption,
} from "@/apps/produce/api/statistics";
import { downloadFile } from "@/utils/tool";
interface StatisticsRow {
user_code?: string;
operator?: string;
production_process?: string;
process_name?: string;
step_name?: string;
total?: number;
[key: string]: string | number | null | undefined;
}
defineOptions({ name: "ProduceDailyWorkStatistics" });
const date = ref(new Date().toISOString().slice(0, 10));
const filters = reactive({
data_type: "all",
user_code: "",
operator: "",
process_id: undefined as string | number | undefined,
step_id: undefined as string | number | undefined,
});
const columns = ref<WorkStatisticsColumn[]>([]);
const items = ref<StatisticsRow[]>([]);
const processOptions = ref<WorkStatisticsOption[]>([]);
const stepOptions = ref<WorkStatisticsOption[]>([]);
const loading = ref(false);
const tableWidth = computed(() => `${590 + columns.value.length * 64}px`);
function spanMethod({
rowIndex,
columnIndex,
}: {
rowIndex: number;
columnIndex: number;
}) {
if (columnIndex !== 1 && columnIndex !== 2) {
return [1, 1];
}
const current = items.value[rowIndex];
if (current.user_code === "总计") {
return [1, 1];
}
const previous = items.value[rowIndex - 1];
if (
previous &&
previous.user_code !== "总计" &&
current.user_code === previous.user_code &&
current.operator === previous.operator
) {
return [0, 0];
}
let rowspan = 1;
for (let index = rowIndex + 1; index < items.value.length; index++) {
const next = items.value[index];
if (
next.user_code === "总计" ||
next.user_code !== current.user_code ||
next.operator !== current.operator
) {
break;
}
rowspan++;
}
return [rowspan, 1];
}
function rowClassName({ row }: { row: StatisticsRow }) {
return row.user_code === "总计" ? "summary-row" : "";
}
function exportData() {
const fields = [
{ key: "user_code", label: "工位号" },
{ key: "operator", label: "操作人员" },
{ key: "production_process", label: "生产工序" },
{ key: "total", label: "总数" },
...columns.value,
];
const escapeCsv = (value: unknown) =>
`"${String(value ?? "").replaceAll('"', '""')}"`;
const csv = [
fields.map((field) => escapeCsv(field.label)).join(","),
...items.value.map((item) =>
fields.map((field) => escapeCsv(item[field.key])).join(","),
),
].join("\r\n");
downloadFile(
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }),
`日工作统计_${date.value}.csv`,
);
}
async function loadData() {
loading.value = true;
try {
const response = await ProduceWorkStatisticsService.daily({
date: date.value,
...filters,
});
columns.value = response?.columns || [];
items.value = (response?.items || []) as StatisticsRow[];
} finally {
loading.value = false;
}
}
async function resetFilters() {
date.value = new Date().toISOString().slice(0, 10);
filters.data_type = "all";
filters.user_code = "";
filters.operator = "";
filters.process_id = undefined;
filters.step_id = undefined;
await loadStepOptions();
await loadData();
}
async function loadStepOptions() {
stepOptions.value = await ProduceWorkStatisticsService.getStepOptions(
filters.process_id,
);
}
async function handleProcessChange() {
filters.step_id = undefined;
await loadStepOptions();
}
onMounted(async () => {
const [processes] = await Promise.all([
ProduceWorkStatisticsService.getProcessOptions(),
loadStepOptions(),
]);
processOptions.value = processes;
await loadData();
});
</script>
<style scoped>
.statistics-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.toolbar .el-input,
.toolbar .el-select {
width: 180px;
}
.table-wrapper {
flex: 1;
min-height: 0;
margin-top: 12px;
background: var(--el-bg-color);
overflow: hidden;
}
.table-scroll {
width: 100%;
height: 100%;
overflow: auto;
}
:deep(.el-table__header-wrapper th .cell) {
white-space: nowrap;
}
:deep(.el-table .cell) {
overflow: visible;
text-overflow: clip;
white-space: nowrap;
}
:deep(.el-table .summary-row) {
--el-table-tr-bg-color: #fff7d6;
}
:deep(.el-table .summary-row > td.el-table__cell) {
background-color: #fff7d6 !important;
}
:deep(.el-table .summary-row:hover > td.el-table__cell) {
background-color: #fff2b8 !important;
}
</style>
@@ -0,0 +1,290 @@
<template>
<div class="statistics-page art-full-height">
<div class="toolbar">
<el-date-picker
v-model="month"
type="month"
value-format="YYYY-MM"
placeholder="选择月份"
@change="loadData"
/>
<el-select v-model="filters.data_type" placeholder="数据类型" @change="loadData">
<el-option label="全部数据" value="all" />
<el-option label="订单数据" value="order" />
<el-option label="整机数据" value="machine" />
</el-select>
<el-input
v-model="filters.user_code"
clearable
placeholder="用户编码"
@keyup.enter="loadData"
/>
<el-input
v-model="filters.operator"
clearable
placeholder="操作人员"
@keyup.enter="loadData"
/>
<el-select
v-model="filters.process_id"
clearable
filterable
placeholder="选择工序名称"
@change="handleProcessChange"
>
<el-option
v-for="option in processOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-select
v-model="filters.step_id"
clearable
filterable
placeholder="选择步骤名称"
>
<el-option
v-for="option in stepOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-button type="primary" :loading="loading" @click="loadData"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
<el-button :disabled="!items.length" @click="exportData">导出</el-button>
</div>
<div class="table-wrapper" v-loading="loading">
<el-table
:data="items"
border
stripe
height="100%"
empty-text="暂无统计数据"
:span-method="spanMethod"
:row-class-name="rowClassName"
>
<el-table-column type="index" label="序号" width="65" fixed="left" />
<el-table-column
prop="user_code"
label="工位号"
width="110"
fixed="left"
/>
<el-table-column
prop="operator"
label="操作人员"
width="120"
fixed="left"
/>
<el-table-column prop="production_process" label="生产工序" width="220" fixed="left" />
<el-table-column prop="total" label="总数" width="75" fixed="left" />
<el-table-column
v-for="column in columns"
:key="column.key"
:prop="column.key"
:label="column.label"
width="58"
align="center"
/>
</el-table>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import {
ProduceWorkStatisticsService,
type WorkStatisticsColumn,
type WorkStatisticsOption,
} from "@/apps/produce/api/statistics";
import { downloadFile } from "@/utils/tool";
interface StatisticsRow {
user_code?: string;
operator?: string;
production_process?: string;
process_name?: string;
step_name?: string;
total?: number;
[key: string]: string | number | null | undefined;
}
defineOptions({ name: "ProduceMonthlyWorkStatistics" });
const month = ref(new Date().toISOString().slice(0, 7));
const filters = reactive({
data_type: "all",
user_code: "",
operator: "",
process_id: undefined as string | number | undefined,
step_id: undefined as string | number | undefined,
});
const columns = ref<WorkStatisticsColumn[]>([]);
const items = ref<StatisticsRow[]>([]);
const processOptions = ref<WorkStatisticsOption[]>([]);
const stepOptions = ref<WorkStatisticsOption[]>([]);
const loading = ref(false);
function spanMethod({
rowIndex,
columnIndex,
}: {
rowIndex: number;
columnIndex: number;
}) {
if (columnIndex !== 1 && columnIndex !== 2) {
return [1, 1];
}
const current = items.value[rowIndex];
if (current.user_code === "总计") {
return [1, 1];
}
const previous = items.value[rowIndex - 1];
if (
previous &&
previous.user_code !== "总计" &&
current.user_code === previous.user_code &&
current.operator === previous.operator
) {
return [0, 0];
}
let rowspan = 1;
for (let index = rowIndex + 1; index < items.value.length; index++) {
const next = items.value[index];
if (
next.user_code === "总计" ||
next.user_code !== current.user_code ||
next.operator !== current.operator
) {
break;
}
rowspan++;
}
return [rowspan, 1];
}
function rowClassName({ row }: { row: StatisticsRow }) {
return row.user_code === "总计" ? "summary-row" : "";
}
function exportData() {
const fields = [
{ key: "user_code", label: "工位号" },
{ key: "operator", label: "操作人员" },
{ key: "production_process", label: "生产工序" },
{ key: "total", label: "总数" },
...columns.value,
];
const escapeCsv = (value: unknown) =>
`"${String(value ?? "").replaceAll('"', '""')}"`;
const csv = [
fields.map((field) => escapeCsv(field.label)).join(","),
...items.value.map((item) =>
fields.map((field) => escapeCsv(item[field.key])).join(","),
),
].join("\r\n");
downloadFile(
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }),
`月工作统计_${month.value}.csv`,
);
}
async function loadData() {
loading.value = true;
try {
const response = await ProduceWorkStatisticsService.monthly({
month: month.value,
...filters,
});
columns.value = response?.columns || [];
items.value = (response?.items || []) as StatisticsRow[];
} finally {
loading.value = false;
}
}
async function resetFilters() {
month.value = new Date().toISOString().slice(0, 7);
filters.data_type = "all";
filters.user_code = "";
filters.operator = "";
filters.process_id = undefined;
filters.step_id = undefined;
await loadStepOptions();
await loadData();
}
async function loadStepOptions() {
stepOptions.value = await ProduceWorkStatisticsService.getStepOptions(
filters.process_id,
);
}
async function handleProcessChange() {
filters.step_id = undefined;
await loadStepOptions();
}
onMounted(async () => {
const [processes] = await Promise.all([
ProduceWorkStatisticsService.getProcessOptions(),
loadStepOptions(),
]);
processOptions.value = processes;
await loadData();
});
</script>
<style scoped>
.statistics-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.toolbar .el-input,
.toolbar .el-select {
width: 180px;
}
:deep(.el-table .summary-row) {
--el-table-tr-bg-color: #fff7d6;
}
:deep(.el-table .summary-row > td.el-table__cell) {
background-color: #fff7d6 !important;
}
:deep(.el-table .summary-row:hover > td.el-table__cell) {
background-color: #fff2b8 !important;
}
.table-wrapper {
flex: 1;
min-height: 0;
margin-top: 12px;
background: var(--el-bg-color);
}
</style>
@@ -0,0 +1,26 @@
<template>
<div class="produce-status-transition-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStatusTransitionList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-status-transition-page {
height: 100%;
}
</style>
@@ -0,0 +1,290 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStatusTransitionService } from '../../../../api/index'
const triggerEventOptions = [
{ label: '下一工序扫码触发', value: 'next_step_scan' },
{ label: '扫描触发', value: 'scan_trigger' },
{ label: '异常触发', value: 'exception_trigger' },
{ label: '手动触发', value: 'manual_trigger' },
{ label: '定时触发', value: 'timer_trigger' },
{ label: '完成触发', value: 'complete_trigger' },
]
const transitionTypeOptions = [
{ label: '主转换', value: 'primary' },
{ label: '副作用', value: 'side_effect' },
]
const sideEffectTypeOptions = [
{ label: '记录日志', value: 'log' },
{ label: '发送通知', value: 'notify' },
{ label: '更新字段', value: 'update_field' },
{ label: '触发事件', value: 'trigger_event' },
]
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStatusTransitionService.list,
dialogTitle: $t('produce.modules.status_transition.title'),
crudApi: {
add: ProduceStatusTransitionService.create,
edit: ProduceStatusTransitionService.update,
remove: ProduceStatusTransitionService.remove,
view: ProduceStatusTransitionService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:status-transition:create',
editAuth: 'produce:status-transition:update',
removeAuth: 'produce:status-transition:delete',
viewAuth: 'produce:status-transition:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'step_id',
label: $t('produce.modules.status_transition.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'trigger_event',
label: $t('produce.modules.status_transition.search.trigger_event'),
component: 'Select',
componentProps: {
options: triggerEventOptions,
clearable: true,
placeholder: $t('produce.modules.status_transition.search.trigger_event_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.status_transition.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.status_transition.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'step.step_name', label: $t('produce.modules.status_transition.columns.step_name'), minWidth: 160 },
{ prop: 'from_status.status_name', label: $t('produce.modules.status_transition.columns.from_status_name'), minWidth: 140 },
{ prop: 'to_status.status_name', label: $t('produce.modules.status_transition.columns.to_status_name'), minWidth: 140 },
{ prop: 'trigger_event', label: $t('produce.modules.status_transition.columns.trigger_event'), minWidth: 160 },
{ prop: 'transition_type', label: $t('produce.modules.status_transition.columns.transition_type'), width: 120 },
{
prop: 'auto_trigger',
label: $t('produce.modules.status_transition.columns.auto_trigger'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'priority', label: $t('produce.modules.status_transition.columns.priority'), width: 100 },
{ prop: 'sort', label: $t('produce.modules.status_transition.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.status_transition.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.status_transition.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '860px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'step_id',
label: $t('produce.modules.status_transition.form.step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.step_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'from_status_id',
label: $t('produce.modules.status_transition.form.from_status_id'),
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.from_status_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.from_status_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'to_status_id',
label: $t('produce.modules.status_transition.form.to_status_id'),
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.to_status_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.to_status_id_placeholder') }],
colSpan: 12,
},
{
component: 'Select',
prop: 'trigger_event',
label: $t('produce.modules.status_transition.columns.trigger_event'),
componentProps: {
options: triggerEventOptions,
placeholder: $t('produce.modules.status_transition.form.trigger_event_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.trigger_event_placeholder') }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'transition_type',
label: $t('produce.modules.status_transition.columns.transition_type'),
defaultValue: 'primary',
componentProps: {
options: transitionTypeOptions,
isBtn: true,
renderType: 'RadioButton',
},
colSpan: 24,
},
{
component: 'Select',
prop: 'side_effect_type',
label: $t('produce.modules.status_transition.form.side_effect_type'),
componentProps: {
options: sideEffectTypeOptions,
clearable: true,
placeholder: $t('produce.modules.status_transition.form.side_effect_type_placeholder'),
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'auto_trigger',
label: $t('produce.modules.status_transition.columns.auto_trigger'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'priority',
label: $t('produce.modules.status_transition.columns.priority'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.status_transition.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'event_config',
label: $t('produce.modules.status_transition.form.event_config'),
componentProps: {
type: 'textarea',
rows: 5,
placeholder: $t('produce.modules.status_transition.form.event_config_placeholder'),
},
colSpan: 24,
},
{
component: 'Input',
prop: 'side_effect_config',
label: $t('produce.modules.status_transition.form.side_effect_config'),
componentProps: {
type: 'textarea',
rows: 5,
placeholder: $t('produce.modules.status_transition.form.side_effect_config_placeholder'),
},
colSpan: 24,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.status_transition.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.status_transition.form.description'),
componentProps: {
type: 'textarea',
rows: 3,
placeholder: $t('produce.modules.status_transition.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-step-relation-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStepRelationList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-step-relation-page {
height: 100%;
}
</style>
@@ -0,0 +1,218 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStepRelationService } from '../../../../api/index'
const relationTypeOptions = [
{ label: '正常流转', value: 'normal' },
{ label: '异常流转', value: 'exception' },
]
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStepRelationService.list,
dialogTitle: $t('produce.modules.step_relation.title'),
crudApi: {
add: ProduceStepRelationService.create,
edit: ProduceStepRelationService.update,
remove: ProduceStepRelationService.remove,
view: ProduceStepRelationService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:step-relation:create',
editAuth: 'produce:step-relation:update',
removeAuth: 'produce:step-relation:delete',
viewAuth: 'produce:step-relation:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'from_step_id',
label: $t('produce.modules.step_relation.search.from_step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.search.from_step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'to_step_id',
label: $t('produce.modules.step_relation.search.to_step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.search.to_step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'relation_type',
label: $t('produce.modules.step_relation.search.relation_type'),
component: 'Select',
componentProps: {
options: relationTypeOptions,
clearable: true,
placeholder: $t('produce.modules.step_relation.search.relation_type_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.step_relation.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.step_relation.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'from_step.step_name', label: $t('produce.modules.step_relation.columns.from_step_name'), minWidth: 180 },
{ prop: 'to_step.step_name', label: $t('produce.modules.step_relation.columns.to_step_name'), minWidth: 180 },
{ prop: 'relation_type', label: $t('produce.modules.step_relation.columns.relation_type'), width: 120 },
{ prop: 'priority', label: $t('produce.modules.step_relation.columns.priority'), width: 100 },
{ prop: 'flow_condition', label: $t('produce.modules.step_relation.columns.flow_condition'), minWidth: 220 },
{ prop: 'sort', label: $t('produce.modules.step_relation.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.step_relation.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.step_relation.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'from_step_id',
label: $t('produce.modules.step_relation.form.from_step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.form.from_step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_relation.form.from_step_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'to_step_id',
label: $t('produce.modules.step_relation.form.to_step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.form.to_step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_relation.form.to_step_id_placeholder') }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'relation_type',
label: $t('produce.modules.step_relation.columns.relation_type'),
defaultValue: 'normal',
componentProps: {
options: relationTypeOptions,
isBtn: true,
renderType: 'RadioButton',
},
colSpan: 24,
},
{
component: 'InputNumber',
prop: 'priority',
label: $t('produce.modules.step_relation.columns.priority'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.step_relation.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'flow_condition',
label: $t('produce.modules.step_relation.form.flow_condition'),
componentProps: {
type: 'textarea',
rows: 6,
placeholder: $t('produce.modules.step_relation.form.flow_condition_placeholder'),
},
colSpan: 24,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.step_relation.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.step_relation.form.description'),
componentProps: {
type: 'textarea',
rows: 3,
placeholder: $t('produce.modules.step_relation.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-step-status-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStepStatusList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-step-status-page {
height: 100%;
}
</style>
@@ -0,0 +1,227 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStepStatusService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStepStatusService.list,
dialogTitle: $t('produce.modules.step_status.title'),
crudApi: {
add: ProduceStepStatusService.create,
edit: ProduceStepStatusService.update,
remove: ProduceStepStatusService.remove,
view: ProduceStepStatusService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:step-status:create',
editAuth: 'produce:step-status:update',
removeAuth: 'produce:step-status:delete',
viewAuth: 'produce:step-status:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'step_id',
label: $t('produce.modules.step_status.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_status.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_status_code',
label: $t('produce.modules.step_status.search.status_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_status_name',
label: $t('produce.modules.step_status.search.status_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.step_status.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{
prop: 'step.step_name',
label: $t('produce.modules.step_status.columns.step_name'),
minWidth: 180,
},
{ prop: 'status_code', label: $t('produce.modules.step_status.columns.status_code'), minWidth: 160 },
{ prop: 'status_name', label: $t('produce.modules.step_status.columns.status_name'), minWidth: 180 },
{
prop: 'is_initial',
label: $t('produce.modules.step_status.columns.is_initial'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{
prop: 'is_final',
label: $t('produce.modules.step_status.columns.is_final'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'sort', label: $t('produce.modules.step_status.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.step_status.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.step_status.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '720px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'step_id',
label: $t('produce.modules.step_status.form.step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_status.form.step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.step_id_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'status_code',
label: $t('produce.modules.step_status.columns.status_code'),
componentProps: {
placeholder: $t('produce.modules.step_status.form.status_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.status_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'status_name',
label: $t('produce.modules.step_status.columns.status_name'),
componentProps: {
placeholder: $t('produce.modules.step_status.form.status_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.status_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.step_status.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_initial',
label: $t('produce.modules.step_status.columns.is_initial'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_final',
label: $t('produce.modules.step_status.columns.is_final'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.step_status.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.step_status.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.step_status.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-transition-log-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas/index'
defineOptions({ name: 'ProduceTransitionLogList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-transition-log-page {
height: 100%;
}
</style>
@@ -0,0 +1,277 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceTransitionLogService } from '../../../../api/index'
const triggerEventOptions = [
{ label: '下一工序扫码触发', value: 'next_step_scan' },
{ label: '扫描触发', value: 'scan_trigger' },
{ label: '异常触发', value: 'exception_trigger' },
{ label: '手动触发', value: 'manual_trigger' },
{ label: '定时触发', value: 'timer_trigger' },
{ label: '完成触发', value: 'complete_trigger' },
]
const formatJsonText = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return ''
}
if (typeof value === 'string') {
return value
}
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
export const crudSchema = (): CrudSchema => {
return {
api: ProduceTransitionLogService.list,
dialogTitle: $t('produce.modules.transition_log.title'),
crudApi: {
view: async (id: string | number) => {
const res = await ProduceTransitionLogService.get(id)
return {
...res,
trigger_data: formatJsonText(res?.trigger_data),
}
},
},
useCrud: true,
hasAdd: false,
hasEdit: false,
hasRemove: false,
hasView: true,
viewAuth: 'produce:transition-log:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_work_order_no',
label: $t('produce.modules.transition_log.search.work_order_no'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.transition_log.search.work_order_no_placeholder'),
},
colSpan: 6,
},
{
prop: 'step_id',
label: $t('produce.modules.transition_log.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.transition_log.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'trigger_event',
label: $t('produce.modules.transition_log.search.trigger_event'),
component: 'Select',
componentProps: {
options: triggerEventOptions,
clearable: true,
placeholder: $t('produce.modules.transition_log.search.trigger_event_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_trigger_user',
label: $t('produce.modules.transition_log.search.trigger_user'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.transition_log.search.trigger_user_placeholder'),
},
colSpan: 6,
},
{
prop: 'is_success',
label: $t('produce.modules.transition_log.search.is_success'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.transition_log.search.is_success_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'work_order_no', label: $t('produce.modules.transition_log.columns.work_order_no'), minWidth: 140 },
{ prop: 'step.step_name', label: $t('produce.modules.transition_log.columns.step_name'), minWidth: 160 },
{ prop: 'from_status.status_name', label: $t('produce.modules.transition_log.columns.from_status_name'), minWidth: 140 },
{ prop: 'to_status.status_name', label: $t('produce.modules.transition_log.columns.to_status_name'), minWidth: 140 },
{
prop: 'trigger_event',
label: $t('produce.modules.transition_log.columns.trigger_event'),
minWidth: 160,
formatter: (row: { trigger_event?: string }) =>
triggerEventOptions.find((option) => option.value === row.trigger_event)?.label || row.trigger_event || ''
},
{ prop: 'trigger_user', label: $t('produce.modules.transition_log.columns.trigger_user'), minWidth: 120 },
{
prop: 'is_auto',
label: $t('produce.modules.transition_log.columns.is_auto'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{
prop: 'is_success',
label: $t('produce.modules.transition_log.columns.is_success'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'duration', label: $t('produce.modules.transition_log.columns.duration'), width: 120 },
{ prop: 'error_message', label: $t('produce.modules.transition_log.columns.error_message'), minWidth: 200 },
{ prop: 'created_date', label: $t('produce.modules.transition_log.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '960px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 12,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'work_order_no',
label: $t('produce.modules.transition_log.columns.work_order_no'),
colSpan: 12,
},
{
component: 'Input',
prop: 'step.step_name',
label: $t('produce.modules.transition_log.columns.step_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'transition.trigger_event',
label: $t('produce.modules.transition_log.columns.transition_event'),
colSpan: 12,
},
{
component: 'Input',
prop: 'from_status.status_name',
label: $t('produce.modules.transition_log.columns.from_status_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'to_status.status_name',
label: $t('produce.modules.transition_log.columns.to_status_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_event',
label: $t('produce.modules.transition_log.columns.trigger_event'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_user',
label: $t('produce.modules.transition_log.columns.trigger_user'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_ip',
label: $t('produce.modules.transition_log.columns.trigger_ip'),
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_auto',
label: $t('produce.modules.transition_log.columns.is_auto'),
componentProps: {
code: DictEnum.SYS_YES_NO,
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_success',
label: $t('produce.modules.transition_log.columns.is_success'),
componentProps: {
code: DictEnum.SYS_YES_NO,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'duration',
label: $t('produce.modules.transition_log.columns.duration'),
colSpan: 12,
},
{
component: 'Input',
prop: 'created_name',
label: $t('produce.modules.transition_log.columns.created_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'created_date',
label: $t('produce.modules.transition_log.columns.created_at'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_data',
label: $t('produce.modules.transition_log.columns.trigger_data'),
componentProps: {
type: 'textarea',
rows: 6,
},
colSpan: 24,
},
{
component: 'Input',
prop: 'error_message',
label: $t('produce.modules.transition_log.columns.error_message'),
componentProps: {
type: 'textarea',
rows: 4,
},
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: $t('produce.modules.transition_log.columns.remark'),
componentProps: {
type: 'textarea',
rows: 4,
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,167 @@
import request from '~/utils/request'
export interface ProduceOption {
label: string
value: string | number
}
export interface ProduceRecordItem {
id?: string | number
order_no?: string
process_order_no?: string
product_code?: string
quantity?: number
user_code?: string
operator?: string
scan_result?: number
scan_date?: string
created_date?: string
process?: {
process_name?: string
}
step?: {
step_name?: string
}
status?: {
status_name?: string
}
}
export interface ProduceSubmitScanDataResult {
code: number
msg: string
data?: {
id?: string | number
}
}
export interface ProduceStatistics {
today_total: number
success_total: number
failed_total: number
}
export interface ProduceUserStatsItem {
user_code: string
operator?: string
count: number
}
export interface ProduceMemberUserCheckResult {
id: number
username: string
nickname: string
}
export interface ProduceDataEntryStepRule {
id?: string | number
process_id?: string | number
process_name?: string
step_code?: string
step_name?: string
prerequisite_steps?: Array<string | number>
prerequisite_mode?: string
prerequisite_require_final?: number
allow_repeat_scan?: number
repeat_scan_statuses?: Array<string | number>
}
export interface ProduceDataEntrySpecificRule {
id?: string | number
rule_name?: string
rule_pattern?: string
rule_type?: number
apply_mode?: 'order' | 'machine' | 'both'
match_field?: 'order_no' | 'process_order_no' | 'product_code' | 'user_code'
step_ids?: Array<string | number>
sort?: number
remark?: string
}
export interface ProduceDataEntryConfig {
basic_setting: Record<string, any>
step_rule: ProduceDataEntryStepRule | null
specific_rules: ProduceDataEntrySpecificRule[]
current?: {
process_id?: string | number
step_id?: string | number
order_no?: string
apply_mode?: 'order' | 'machine' | 'both'
}
}
export function getProcessOptions() {
return request.get<ProduceOption[]>('/produce/process', { format: 'select' })
}
export function getStepOptions(processId?: string | number) {
return request.get<ProduceOption[]>('/produce/process-step', {
format: 'select',
...(processId ? { process_id: processId } : {}),
})
}
export function getStatusOptions(stepId?: string | number) {
return request.get<ProduceOption[]>('/produce/step-status', {
format: 'select',
...(stepId ? { step_id: stepId } : {}),
})
}
export function getProduceConfig(groupCode: string) {
return request.get<Record<string, any>>('/produce/config/info', { group_code: groupCode })
}
export function getDataEntryConfig(params: Record<string, any>) {
return request.get<ProduceDataEntryConfig>('/produce/config/data-entry', params)
}
export function getRecentRecords(params: Record<string, any>) {
return request.get<{ items: ProduceRecordItem[]; total: number }>('/produce/scan-data', {
limit: 10,
page: 1,
...params,
})
}
export function getProductRecentRecords(params: Record<string, any>) {
return request.get<{ items: ProduceRecordItem[]; total: number }>('/produce/product-record/recent', {
limit: 10,
page: 1,
...params,
})
}
export function getStatistics(params: Record<string, any>) {
return request.get<ProduceStatistics>('/produce/scan-data/statistics', params, { showErrorMessage: false })
}
export function getProductStatistics(params: Record<string, any>) {
return request.get<ProduceStatistics>('/produce/product-record/statistics', params, { showErrorMessage: false })
}
export function getUserCodeStats(params: Record<string, any>) {
return request.get<ProduceUserStatsItem[]>('/produce/scan-data/user-code-stats', params, { showErrorMessage: false })
}
export function getProductUserCodeStats(params: Record<string, any>) {
return request.get<ProduceUserStatsItem[]>('/produce/product-record/user-code-stats', params, { showErrorMessage: false })
}
export function checkMemberUser(userCode: string) {
return request.get<ProduceMemberUserCheckResult>('/produce/member-user/check', { user_code: userCode }, { showErrorMessage: false })
}
export function submitScanData(data: Record<string, any>) {
return request.post<ProduceSubmitScanDataResult>('/produce/scan-data/record', data, {
showErrorMessage: false,
returnFullResponse: true,
})
}
export function submitProductRecord(data: Record<string, any>) {
return request.post<ProduceSubmitScanDataResult>('/produce/product-record/record', data, {
showErrorMessage: false,
returnFullResponse: true,
})
}
@@ -0,0 +1,143 @@
import request from '~/utils/request'
export interface ProduceExceptionSetting {
id: string | number
parent_id?: string | number | null
setting_type: 'type' | 'reason' | 'issue' | string
code?: string
name: string
sort?: number
frequency?: number
remark?: string
}
export interface ProduceExceptionRecord {
id: string | number
order_no?: string
user_code?: string
reporter?: string
type_id?: string | number
type_name?: string
reason_id?: string | number
reason_name?: string
issue_id?: string | number | null
issue_name?: string
images?: string[]
remark?: string
status?: number
status_name?: string
reported_date?: string
handled_date?: string
handler?: string
}
export interface ProduceRepairTask {
id: string | number
device_no: string
type_name?: string
reason_name?: string
issue_name?: string
reporter?: string
user_code?: string
reported_date?: string
status?: number
status_name?: string
}
export interface ProduceRepairPart {
product_code: string
product_name?: string
category_id?: string | number
category_name?: string
source?: 'new' | 'after_sales' | 'refurbished' | string
quantity?: number
fault_description_ids?: Array<string | number>
}
export interface ProduceScanRecord {
id: string | number
process_name?: string
step_name?: string
operator?: string
user_code?: string
}
export function getExceptionSettings() {
return request.get<ProduceExceptionSetting[]>('/produce/exception/settings', {}, { showErrorMessage: false })
}
export function getExceptionStatistics(params: Record<string, any> = {}) {
return request.get<{ pending: number; review: number; done: number }>('/produce/exception/statistics', params, { showErrorMessage: false })
}
export function getRecentExceptions(params: Record<string, any> = {}) {
return request.get<ProduceExceptionRecord[]>('/produce/exception/recent', params, { showErrorMessage: false })
}
export function checkExceptionOrder(orderNo: string) {
return request.get<Record<string, any>>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false })
}
export function submitExceptionRecord(data: Record<string, any>) {
return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, {
showErrorMessage: false,
returnFullResponse: true,
})
}
export function getRepairTasks(params: Record<string, any> = {}) {
return request.get<ProduceRepairTask[]>('/produce/repair-record/tasks', params, { showErrorMessage: false })
}
export function getRepairStatistics(params: Record<string, any> = {}) {
return request.get<{ pending: number; processing: number; completed_today: number }>('/produce/repair-record/statistics', params, { showErrorMessage: false })
}
export function getRepairTask(deviceNo: string) {
return request.get<ProduceRepairTask>('/produce/exception/repair-task', { device_no: deviceNo.trim() }, { showErrorMessage: false })
}
export function getScanRecords(params: Record<string, any> = {}) {
return request.get<{ items: ProduceScanRecord[]; total: number }>('/produce/scan-data', {
limit: 50,
page: 1,
...params,
}, { showErrorMessage: false })
}
export function getMaterialCategories() {
return request.get<Array<{ id: string | number; category_name: string }>>('/produce/scanapp/material-category', {}, { showErrorMessage: false })
}
export function getFaultDescriptionOptions(categoryIds: Array<string | number>) {
return request.get<Array<{ id: string | number; category_id: string | number; description: string }>>(
'/produce/hardware-fault-description/options',
{ category_ids: categoryIds.join(',') },
{ showErrorMessage: false },
)
}
export function lookupMaterial(productCode: string) {
return request.get<{
id: string | number
category_id: string | number
category_name: string
product_code: string
product_name: string
}>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false })
}
export function submitRepairRecord(data: Record<string, any>) {
return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, {
showErrorMessage: false,
returnFullResponse: true,
})
}
export async function uploadProduceImage(file: File, subDir: 'exception' | 'repair') {
const formData = new FormData()
formData.append('file', file)
formData.append('sub_dir', subDir)
const result = await request.upload<Record<string, any>>('/file/image', formData)
return result?.url || result?.full_url || result?.path || ''
}
@@ -0,0 +1,54 @@
import request from '~/utils/request'
export type ProduceQueryRecordType = 'scan' | 'product' | 'exception' | 'repair'
export interface ProduceQueryRecord {
id: string
record_type: ProduceQueryRecordType
record_type_name: string
order_no?: string
product_code?: string
process_name?: string
exception_reason?: string
exception_content?: string
material_category?: string
fault_description?: string
quantity?: number | null
event_at?: number
event_date?: string | null
remark?: string
}
export interface ProduceQueryPage {
items: ProduceQueryRecord[]
total: number
page: number
limit: number
}
export interface ProduceQueryParams {
record_type: ProduceQueryRecordType
order_no?: string
keyword?: string
user_code?: string
status?: string | number
scan_result?: string | number
start_date?: string
end_date?: string
page?: number
limit?: number
}
export interface ProduceQueryTimeline {
order_no: string
total: number
items: ProduceQueryRecord[]
}
export function queryProduceRecords(params: ProduceQueryParams) {
return request.get<ProduceQueryPage>('/produce/query/records', params, { showErrorMessage: false })
}
export function queryProduceTimeline(orderNo: string) {
return request.get<ProduceQueryTimeline>('/produce/query/timeline', { order_no: orderNo.trim() }, { showErrorMessage: false })
}
@@ -0,0 +1,549 @@
<template>
<div class="entry-mode-panel">
<div class="main-top-grid">
<section class="workbench-panel entry-column-panel">
<div class="panel-header column-header">
<div>
<div class="panel-title">整机扫码输入</div>
<div class="panel-subtitle">当前工序:{{ processLabel || '未选择' }} / {{ stepLabel || '未选择' }}</div>
</div>
<div class="panel-metric-card">
<span class="panel-metric-label">总数</span>
<strong class="panel-metric-value">{{ hideProcessTotal ? '***' : statistics.today_total }}</strong>
<button type="button" class="metric-eye-button" :class="{ 'is-hidden': hideProcessTotal }" @click="hideProcessTotal = !hideProcessTotal">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5C6.5 5 2.12 8.44 1 12c1.12 3.56 5.5 7 11 7s9.88-3.44 11-7c-1.12-3.56-5.5-7-11-7Zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9Zm0-2.5A2 2 0 1 0 12 10a2 2 0 0 0 0 4Z" fill="currentColor" />
</svg>
</button>
</div>
</div>
<div class="workbench-form">
<div class="field-group field-primary">
<label>加工单号</label>
<el-input
ref="processOrderNoInputRef"
v-model="processOrderNo"
clearable
size="large"
placeholder="请扫描或输入加工单号"
@keyup.enter="handleProcessOrderNoComplete"
/>
</div>
<div class="field-row field-row-split">
<div class="field-group">
<label>商品编码</label>
<el-input
ref="productCodeInputRef"
v-model="productCode"
clearable
size="large"
placeholder="请扫描或输入商品编码"
@keyup.enter="handleProductCodeComplete"
/>
</div>
<div class="field-group field-group-quantity">
<label>数量</label>
<el-input-number
ref="quantityInputRef"
v-model="quantity"
:min="1"
:step="1"
size="large"
controls-position="right"
class="quantity-input"
@keyup.enter="handleQuantityComplete"
/>
</div>
</div>
<div class="field-row">
<div class="field-group">
<div class="field-label-row">
<label>用户编码</label>
<div class="lock-switch">
<span>锁定用户编码</span>
<el-switch v-model="autoUserCode" />
</div>
</div>
<el-input
ref="userCodeInputRef"
v-model="userCode"
:disabled="autoUserCode"
clearable
size="large"
placeholder="请扫描或输入用户编码"
@update:model-value="userCode = String($event || '').toUpperCase()"
@input="handleUserCodeInput"
@keyup.enter="handleUserCodeComplete"
@blur="runUserCodeCheck(userCode, true)"
/>
<div v-if="userCodeCheckError" class="field-error-tip">{{ userCodeCheckError }}</div>
</div>
</div>
<div class="action-row">
<el-button type="primary" size="large" :loading="submitting" @click="submitEntry">提交</el-button>
<el-button size="large" @click="resetForm">重置输入</el-button>
</div>
</div>
</section>
<section class="workbench-panel stats-column-panel">
<div class="dashboard-header column-header">
<div class="dashboard-title">工位统计</div>
<span class="dashboard-caption">当日扫码量</span>
</div>
<div v-if="userStats.length" class="station-grid">
<div v-for="item in userStats" :key="item.user_code" class="station-card">
<div class="station-icon">工</div>
<div class="station-info">
<strong>{{ item.user_code }}</strong>
</div>
<b>{{ item.count }}</b>
</div>
</div>
<el-empty v-else description="暂无数据" :image-size="72" />
</section>
</div>
<section class="records-panel">
<div class="dashboard-header records-header">
<div class="dashboard-title">最近记录</div>
<span class="dashboard-caption">实时刷新最近 10 条</span>
</div>
<el-table :data="recentRecords" stripe>
<el-table-column prop="process_order_no" label="加工单号" min-width="180" />
<el-table-column prop="product_code" label="商品编码" min-width="180" />
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="user_code" label="用户编码" min-width="120" />
<el-table-column label="工序" min-width="140">
<template #default="scope">{{ scope.row.process?.process_name || '-' }}</template>
</el-table-column>
<el-table-column label="步骤" min-width="140">
<template #default="scope">{{ scope.row.step?.step_name || '-' }}</template>
</el-table-column>
<el-table-column label="状态" min-width="120">
<template #default="scope">{{ scope.row.status?.status_name || '-' }}</template>
</el-table-column>
<el-table-column label="结果" width="100">
<template #default="scope">
<el-tag :type="Number(scope.row.scan_result) === 1 ? 'success' : 'danger'">
{{ Number(scope.row.scan_result) === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" min-width="120" />
<el-table-column prop="created_date" label="扫码时间" min-width="180" />
</el-table>
</section>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
checkMemberUser,
getDataEntryConfig,
getProductRecentRecords,
getProductStatistics,
getProductUserCodeStats,
submitProductRecord,
type ProduceRecordItem,
type ProduceDataEntrySpecificRule,
type ProduceStatistics,
type ProduceUserStatsItem,
} from '~/apps/produce/api/data-entry'
interface Props {
selectedProcess?: string | number
selectedStep?: string | number
processLabel?: string
stepLabel?: string
scanMode?: 'manual' | 'scanner'
}
const props = withDefaults(defineProps<Props>(), {
selectedProcess: '',
selectedStep: '',
processLabel: '',
stepLabel: '',
scanMode: 'manual',
})
const emit = defineEmits<{
'play-success': []
'play-error': []
'speak-success': []
'speak-error': []
}>()
const STORAGE_KEY_LOCK = 'produce_web_machine_entry_lock'
const STORAGE_KEY_USER_CODE = 'produce_web_machine_entry_user_code'
const STORAGE_KEY_HIDE_PROCESS_TOTAL = 'produce_web_machine_entry_hide_process_total'
const processOrderNo = ref('')
const productCode = ref('')
const quantity = ref(1)
const userCode = ref('')
const autoUserCode = ref(false)
const hideProcessTotal = ref(false)
const submitting = ref(false)
const recentRecords = ref<ProduceRecordItem[]>([])
const userStats = ref<ProduceUserStatsItem[]>([])
const statistics = ref<ProduceStatistics>({ today_total: 0, success_total: 0, failed_total: 0 })
const basicSettings = ref<Record<string, any>>({})
const specificRules = ref<ProduceDataEntrySpecificRule[]>([])
const checkingUserCode = ref(false)
const userCodeCheckError = ref('')
const processOrderNoInputRef = ref()
const productCodeInputRef = ref()
const quantityInputRef = ref()
const userCodeInputRef = ref()
let userCodeCheckTimer: ReturnType<typeof setTimeout> | null = null
let scannerSubmitTimer: ReturnType<typeof setTimeout> | null = null
let statisticsRequestToken = 0
let recentRecordsRequestToken = 0
let dataEntryConfigRequestToken = 0
let initialized = false
const restoreLocalState = () => {
if (typeof window === 'undefined') return
autoUserCode.value = localStorage.getItem(STORAGE_KEY_LOCK) === 'true'
hideProcessTotal.value = localStorage.getItem(STORAGE_KEY_HIDE_PROCESS_TOTAL) === 'true'
if (autoUserCode.value) {
userCode.value = localStorage.getItem(STORAGE_KEY_USER_CODE) || ''
}
}
const saveLocalState = () => {
if (typeof window === 'undefined') return
localStorage.setItem(STORAGE_KEY_LOCK, String(autoUserCode.value))
localStorage.setItem(STORAGE_KEY_HIDE_PROCESS_TOTAL, String(hideProcessTotal.value))
if (autoUserCode.value && userCode.value.trim()) {
localStorage.setItem(STORAGE_KEY_USER_CODE, userCode.value)
} else {
localStorage.removeItem(STORAGE_KEY_USER_CODE)
}
}
const focusPrimaryInput = () => { setTimeout(() => processOrderNoInputRef.value?.focus?.(), 80) }
const focusProductCodeInput = () => { setTimeout(() => productCodeInputRef.value?.focus?.(), 80) }
const focusQuantityInput = () => { setTimeout(() => quantityInputRef.value?.focus?.(), 80) }
const focusUserCodeInput = () => { setTimeout(() => userCodeInputRef.value?.focus?.(), 80) }
const clearScannerSubmitTimer = () => {
if (!scannerSubmitTimer) return
clearTimeout(scannerSubmitTimer)
scannerSubmitTimer = null
}
const handleProcessOrderNoComplete = () => {
if (props.scanMode !== 'scanner') {
void submitEntry()
return
}
if (!submitting.value) focusProductCodeInput()
}
const handleProductCodeComplete = () => {
if (props.scanMode !== 'scanner') {
void submitEntry()
return
}
if (!submitting.value) focusQuantityInput()
}
const handleQuantityComplete = () => {
if (props.scanMode !== 'scanner') return
if (submitting.value) return
if (autoUserCode.value) {
void submitEntry()
return
}
focusUserCodeInput()
}
const handleUserCodeComplete = () => {
if (props.scanMode !== 'scanner') {
void submitEntry()
return
}
if (submitting.value) return
clearScannerSubmitTimer()
userCodeInputRef.value?.blur?.()
scannerSubmitTimer = setTimeout(() => {
scannerSubmitTimer = null
if (!submitting.value) void submitEntry()
}, 50)
}
const handleUserCodeInput = () => {
if (props.scanMode !== 'scanner' || !userCode.value.trim() || submitting.value) return
clearScannerSubmitTimer()
scannerSubmitTimer = setTimeout(() => {
scannerSubmitTimer = null
handleUserCodeComplete()
}, 300)
}
const resetDashboardData = () => {
recentRecords.value = []
userStats.value = []
statistics.value = { today_total: 0, success_total: 0, failed_total: 0 }
}
const isSameSelection = (processId: string | number, stepId: string | number) => String(props.selectedProcess) === String(processId) && String(props.selectedStep) === String(stepId)
const resetUserCodeCheckState = () => { userCodeCheckError.value = '' }
const runUserCodeCheck = async (value: string, silent: boolean = false) => {
const normalizedValue = value.trim()
if (!normalizedValue) {
checkingUserCode.value = false
resetUserCodeCheckState()
return false
}
checkingUserCode.value = true
try {
await checkMemberUser(normalizedValue)
userCodeCheckError.value = ''
return true
} catch (error: any) {
userCodeCheckError.value = error?.message || '用户编码不存在'
if (!silent) ElMessage.warning(userCodeCheckError.value)
return false
} finally {
checkingUserCode.value = false
}
}
const scheduleUserCodeCheck = (value: string) => {
if (userCodeCheckTimer) clearTimeout(userCodeCheckTimer)
const normalizedValue = value.trim()
if (!normalizedValue) {
resetUserCodeCheckState()
checkingUserCode.value = false
return
}
userCodeCheckTimer = setTimeout(() => { void runUserCodeCheck(normalizedValue, true) }, 360)
}
const loadStatisticsData = async () => {
if (!props.selectedProcess || !props.selectedStep) {
resetDashboardData()
return
}
const requestToken = ++statisticsRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const params = { process_id: processSnapshot, step_id: stepSnapshot }
const [statsRes, userStatsRes] = await Promise.all([
getProductStatistics(params).catch(() => ({ today_total: 0, success_total: 0, failed_total: 0 })),
getProductUserCodeStats(params).catch(() => []),
])
if (requestToken !== statisticsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) return
statistics.value = {
today_total: Number(statsRes?.today_total || 0),
success_total: Number(statsRes?.success_total || 0),
failed_total: Number(statsRes?.failed_total || 0),
}
userStats.value = Array.isArray(userStatsRes) ? userStatsRes : []
}
const loadRecentData = async () => {
if (!props.selectedProcess || !props.selectedStep) {
recentRecords.value = []
return
}
const requestToken = ++recentRecordsRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const res = await getProductRecentRecords({ process_id: processSnapshot, step_id: stepSnapshot, limit: 10 }).catch(() => ({ items: [] }))
if (requestToken !== recentRecordsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) return
recentRecords.value = Array.isArray(res?.items) ? res.items : []
}
const loadDataEntryConfig = async () => {
const requestToken = ++dataEntryConfigRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const processOrderNoSnapshot = processOrderNo.value.trim()
const productCodeSnapshot = productCode.value.trim()
const userCodeSnapshot = userCode.value.trim()
const res = await getDataEntryConfig({
process_id: processSnapshot,
step_id: stepSnapshot,
apply_mode: 'machine',
process_order_no: processOrderNoSnapshot,
product_code: productCodeSnapshot,
user_code: userCodeSnapshot,
order_no: processOrderNoSnapshot,
}).catch(() => null)
if (requestToken !== dataEntryConfigRequestToken || !isSameSelection(processSnapshot, stepSnapshot) || processOrderNo.value.trim() !== processOrderNoSnapshot || productCode.value.trim() !== productCodeSnapshot || userCode.value.trim() !== userCodeSnapshot) return
basicSettings.value = res?.basic_setting || {}
specificRules.value = Array.isArray(res?.specific_rules) ? res.specific_rules : []
}
const matchSpecificRule = (rule: ProduceDataEntrySpecificRule, valueMap: Record<string, string>) => {
const matchField = rule.match_field || 'order_no'
const matchValue = String(valueMap[matchField] || '').trim()
const pattern = String(rule.rule_pattern || '').trim()
if (!matchValue || !pattern) {
return false
}
try {
return new RegExp(pattern).test(matchValue)
} catch {
return false
}
}
const validateSpecificRules = () => {
if (!specificRules.value.length) {
return true
}
const context = {
process_order_no: processOrderNo.value.trim(),
product_code: productCode.value.trim(),
user_code: userCode.value.trim(),
}
const blockedRule = specificRules.value.find((rule: ProduceDataEntrySpecificRule) => {
if ((rule.apply_mode || 'order') !== 'machine' && rule.apply_mode !== 'both') {
return false
}
return Number(rule.rule_type) === 2 && matchSpecificRule(rule, context)
})
if (blockedRule) {
ElMessage.warning(blockedRule.rule_name || '当前整机数据命中特定规则,禁止提交')
const matchField = blockedRule.match_field || 'process_order_no'
if (matchField === 'product_code') {
focusProductCodeInput()
} else if (matchField === 'user_code') {
focusUserCodeInput()
} else {
focusPrimaryInput()
}
return false
}
return true
}
const validateBeforeSubmit = () => {
if (!props.selectedProcess) { ElMessage.warning('请选择工序'); return false }
if (!props.selectedStep) { ElMessage.warning('请选择步骤'); return false }
if (!processOrderNo.value.trim()) { ElMessage.warning('请输入加工单号'); focusPrimaryInput(); return false }
const processOrderNoRegex = String(basicSettings.value.process_order_no_regex || '').trim()
if (processOrderNoRegex) {
try {
const regex = new RegExp(processOrderNoRegex)
if (!regex.test(processOrderNo.value.trim())) { ElMessage.warning('加工单号格式不符合生产配置规则'); focusPrimaryInput(); return false }
} catch {}
}
if (!productCode.value.trim()) { ElMessage.warning('请输入商品编码'); focusProductCodeInput(); return false }
const productCodeMinLength = Number(basicSettings.value.product_code_min_length || 0)
if (productCodeMinLength > 0 && productCode.value.trim().length < productCodeMinLength) { ElMessage.warning(`商品编码长度不能少于 ${productCodeMinLength} 位`); focusProductCodeInput(); return false }
const productCodeMaxLength = Number(basicSettings.value.product_code_max_length || 0)
if (productCodeMaxLength > 0 && productCode.value.trim().length > productCodeMaxLength) { ElMessage.warning(`商品编码长度不能超过 ${productCodeMaxLength} 位`); focusProductCodeInput(); return false }
const productCodeRegex = String(basicSettings.value.product_code_regex || '').trim()
if (productCodeRegex) {
try {
const regex = new RegExp(productCodeRegex)
if (!regex.test(productCode.value.trim())) { ElMessage.warning('商品编码格式不符合生产配置规则'); focusProductCodeInput(); return false }
} catch {}
}
if (!Number.isFinite(Number(quantity.value)) || Number(quantity.value) < 1) { ElMessage.warning('数量必须大于 0'); focusQuantityInput(); return false }
if (!userCode.value.trim()) { ElMessage.warning('请输入用户编码'); focusUserCodeInput(); return false }
const userCodeRegex = String(basicSettings.value.user_code_regex || '').trim()
if (userCodeRegex) {
try {
const regex = new RegExp(userCodeRegex)
if (!regex.test(userCode.value.trim())) { ElMessage.warning('用户编码格式不符合基础设置规则'); focusUserCodeInput(); return false }
} catch {}
}
if (userCodeCheckError.value) { ElMessage.warning(userCodeCheckError.value); focusUserCodeInput(); return false }
if (!validateSpecificRules()) { return false }
return true
}
const resetForm = () => {
processOrderNo.value = ''
productCode.value = ''
quantity.value = 1
if (!autoUserCode.value) {
userCode.value = ''
resetUserCodeCheckState()
}
saveLocalState()
focusPrimaryInput()
}
const submitEntry = async () => {
if (!validateBeforeSubmit()) return
const passed = await runUserCodeCheck(userCode.value)
if (!passed) { emit('play-error'); nextTick(() => focusUserCodeInput()); return }
submitting.value = true
try {
const result = await submitProductRecord({
process_order_no: processOrderNo.value.trim(),
product_code: productCode.value.trim(),
quantity: Number(quantity.value) || 1,
process_id: props.selectedProcess,
step_id: props.selectedStep,
user_code: userCode.value.trim(),
scan_result: 1,
})
if (Number(result?.code) !== 0) throw new Error(result?.msg || '录入失败')
ElMessage.success('录入成功')
emit('play-success')
emit('speak-success')
saveLocalState()
resetForm()
await Promise.all([loadStatisticsData(), loadRecentData()])
} catch (error: any) {
ElMessage.error(error?.message || '录入失败')
emit('play-error')
emit('speak-error')
} finally {
submitting.value = false
}
}
const refreshBySelection = async () => {
if (!props.selectedProcess || !props.selectedStep) {
basicSettings.value = {}
specificRules.value = []
resetDashboardData()
return
}
resetDashboardData()
await Promise.all([loadStatisticsData(), loadRecentData(), loadDataEntryConfig()])
focusPrimaryInput()
}
watch(() => [props.selectedProcess, props.selectedStep], () => { if (initialized) void refreshBySelection() })
watch(processOrderNo, () => { if (initialized) { saveLocalState(); void loadDataEntryConfig() } })
watch([productCode, quantity], () => { if (initialized) { saveLocalState(); void loadDataEntryConfig() } })
watch(userCode, () => { if (initialized) { scheduleUserCodeCheck(userCode.value); if (autoUserCode.value || userCode.value.trim()) saveLocalState(); void loadDataEntryConfig() } })
watch([autoUserCode, hideProcessTotal], () => { if (initialized) saveLocalState() })
onMounted(async () => {
restoreLocalState()
await refreshBySelection()
initialized = true
})
onBeforeUnmount(() => {
if (userCodeCheckTimer) clearTimeout(userCodeCheckTimer)
clearScannerSubmitTimer()
})
defineExpose({ focusPrimaryInput, focusProductCodeInput, focusQuantityInput, focusUserCodeInput, submitEntry, resetForm })
</script>
<style scoped src="./entry-panel.css"></style>
@@ -0,0 +1,610 @@
<template>
<div class="entry-mode-panel">
<div class="main-top-grid">
<section class="workbench-panel entry-column-panel">
<div class="panel-header column-header">
<div>
<div class="panel-title">订单扫码输入</div>
<div class="panel-subtitle">当前工序:{{ processLabel || '未选择' }} / {{ stepLabel || '未选择' }}</div>
</div>
<div class="panel-metric-card">
<span class="panel-metric-label">总数</span>
<strong class="panel-metric-value">{{ hideProcessTotal ? '***' : statistics.today_total }}</strong>
<button type="button" class="metric-eye-button" :class="{ 'is-hidden': hideProcessTotal }" @click="hideProcessTotal = !hideProcessTotal">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5C6.5 5 2.12 8.44 1 12c1.12 3.56 5.5 7 11 7s9.88-3.44 11-7c-1.12-3.56-5.5-7-11-7Zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9Zm0-2.5A2 2 0 1 0 12 10a2 2 0 0 0 0 4Z" fill="currentColor" />
</svg>
</button>
</div>
</div>
<div class="workbench-form">
<div class="field-group field-primary">
<label>订单号</label>
<el-input
ref="orderNoInputRef"
v-model="orderNo"
clearable
size="large"
placeholder="请扫描或输入订单号"
@keyup.enter="handleOrderNoComplete"
/>
</div>
<div class="field-row">
<div class="field-group">
<div class="field-label-row">
<label>用户编码</label>
<div class="lock-switch">
<span>锁定用户编码</span>
<el-switch v-model="autoUserCode" />
</div>
</div>
<el-input
ref="userCodeInputRef"
v-model="userCode"
:disabled="autoUserCode"
clearable
size="large"
placeholder="请扫描或输入用户编码"
@update:model-value="userCode = String($event || '').toUpperCase()"
@input="handleUserCodeInput"
@keyup.enter="handleUserCodeComplete"
@blur="runUserCodeCheck(userCode, true)"
/>
<div v-if="userCodeCheckError" class="field-error-tip">{{ userCodeCheckError }}</div>
</div>
</div>
<div class="action-row">
<el-button type="primary" size="large" :loading="submitting" @click="submitEntry">提交</el-button>
<el-button size="large" @click="resetForm">重置输入</el-button>
</div>
</div>
</section>
<section class="workbench-panel stats-column-panel">
<div class="dashboard-header column-header">
<div class="dashboard-title">工位统计</div>
<span class="dashboard-caption">当日扫码量</span>
</div>
<div v-if="userStats.length" class="station-grid">
<div v-for="item in userStats" :key="item.user_code" class="station-card">
<div class="station-icon">工</div>
<div class="station-info">
<strong>{{ item.user_code }}</strong>
</div>
<b>{{ item.count }}</b>
</div>
</div>
<el-empty v-else description="暂无数据" :image-size="72" />
</section>
</div>
<section class="records-panel">
<div class="dashboard-header records-header">
<div class="dashboard-title">最近记录</div>
<span class="dashboard-caption">实时刷新最近 10 条</span>
</div>
<el-table :data="recentRecords" stripe>
<el-table-column prop="order_no" label="订单号" min-width="180" />
<el-table-column prop="user_code" label="用户编码" min-width="120" />
<el-table-column label="工序" min-width="140">
<template #default="scope">{{ scope.row.process?.process_name || '-' }}</template>
</el-table-column>
<el-table-column label="步骤" min-width="140">
<template #default="scope">{{ scope.row.step?.step_name || '-' }}</template>
</el-table-column>
<el-table-column label="状态" min-width="120">
<template #default="scope">{{ scope.row.status?.status_name || '-' }}</template>
</el-table-column>
<el-table-column label="结果" width="100">
<template #default="scope">
<el-tag :type="Number(scope.row.scan_result) === 1 ? 'success' : 'danger'">
{{ Number(scope.row.scan_result) === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" min-width="120" />
<el-table-column prop="scan_date" label="扫码时间" min-width="180" />
</el-table>
</section>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
checkMemberUser,
getDataEntryConfig,
getRecentRecords,
getStatistics,
getUserCodeStats,
submitScanData,
type ProduceRecordItem,
type ProduceDataEntrySpecificRule,
type ProduceStatistics,
type ProduceUserStatsItem,
} from '~/apps/produce/api/data-entry'
interface Props {
selectedProcess?: string | number
selectedStep?: string | number
processLabel?: string
stepLabel?: string
scanMode?: 'manual' | 'scanner'
}
const props = withDefaults(defineProps<Props>(), {
selectedProcess: '',
selectedStep: '',
processLabel: '',
stepLabel: '',
scanMode: 'manual',
})
const emit = defineEmits<{
'play-success': []
'play-error': []
'speak-success': []
'speak-error': []
}>()
const STORAGE_KEY_LOCK = 'produce_web_order_entry_lock'
const STORAGE_KEY_USER_CODE = 'produce_web_order_entry_user_code'
const STORAGE_KEY_HIDE_PROCESS_TOTAL = 'produce_web_order_entry_hide_process_total'
const orderNo = ref('')
const userCode = ref('')
const autoUserCode = ref(false)
const hideProcessTotal = ref(false)
const submitting = ref(false)
const recentRecords = ref<ProduceRecordItem[]>([])
const userStats = ref<ProduceUserStatsItem[]>([])
const statistics = ref<ProduceStatistics>({
today_total: 0,
success_total: 0,
failed_total: 0,
})
const basicSettings = ref<Record<string, any>>({})
const specificRules = ref<ProduceDataEntrySpecificRule[]>([])
const checkingUserCode = ref(false)
const userCodeCheckError = ref('')
const orderNoInputRef = ref()
const userCodeInputRef = ref()
let userCodeCheckTimer: ReturnType<typeof setTimeout> | null = null
let scannerSubmitTimer: ReturnType<typeof setTimeout> | null = null
let statisticsRequestToken = 0
let recentRecordsRequestToken = 0
let dataEntryConfigRequestToken = 0
let initialized = false
const restoreLocalState = () => {
if (typeof window === 'undefined') return
autoUserCode.value = localStorage.getItem(STORAGE_KEY_LOCK) === 'true'
hideProcessTotal.value = localStorage.getItem(STORAGE_KEY_HIDE_PROCESS_TOTAL) === 'true'
if (autoUserCode.value) {
userCode.value = localStorage.getItem(STORAGE_KEY_USER_CODE) || ''
}
}
const saveLocalState = () => {
if (typeof window === 'undefined') return
localStorage.setItem(STORAGE_KEY_LOCK, String(autoUserCode.value))
localStorage.setItem(STORAGE_KEY_HIDE_PROCESS_TOTAL, String(hideProcessTotal.value))
if (autoUserCode.value && userCode.value.trim()) {
localStorage.setItem(STORAGE_KEY_USER_CODE, userCode.value)
} else {
localStorage.removeItem(STORAGE_KEY_USER_CODE)
}
}
const focusPrimaryInput = () => {
setTimeout(() => orderNoInputRef.value?.focus?.(), 80)
}
const focusUserCodeInput = () => {
setTimeout(() => userCodeInputRef.value?.focus?.(), 80)
}
const clearScannerSubmitTimer = () => {
if (!scannerSubmitTimer) {
return
}
clearTimeout(scannerSubmitTimer)
scannerSubmitTimer = null
}
const handleOrderNoComplete = () => {
if (props.scanMode !== 'scanner') {
void submitEntry()
return
}
if (submitting.value) {
return
}
if (autoUserCode.value) {
void submitEntry()
return
}
focusUserCodeInput()
}
const handleUserCodeComplete = () => {
if (props.scanMode !== 'scanner') {
void submitEntry()
return
}
if (submitting.value) {
return
}
clearScannerSubmitTimer()
userCodeInputRef.value?.blur?.()
scannerSubmitTimer = setTimeout(() => {
scannerSubmitTimer = null
if (!submitting.value) {
void submitEntry()
}
}, 50)
}
const handleUserCodeInput = () => {
if (props.scanMode !== 'scanner' || !userCode.value.trim() || submitting.value) {
return
}
clearScannerSubmitTimer()
scannerSubmitTimer = setTimeout(() => {
scannerSubmitTimer = null
handleUserCodeComplete()
}, 300)
}
const resetDashboardData = () => {
recentRecords.value = []
userStats.value = []
statistics.value = {
today_total: 0,
success_total: 0,
failed_total: 0,
}
}
const isSameSelection = (processId: string | number, stepId: string | number) => {
return String(props.selectedProcess) === String(processId)
&& String(props.selectedStep) === String(stepId)
}
const resetUserCodeCheckState = () => {
userCodeCheckError.value = ''
}
const runUserCodeCheck = async (value: string, silent: boolean = false) => {
const normalizedValue = value.trim()
if (!normalizedValue) {
checkingUserCode.value = false
resetUserCodeCheckState()
return false
}
checkingUserCode.value = true
try {
await checkMemberUser(normalizedValue)
userCodeCheckError.value = ''
return true
} catch (error: any) {
userCodeCheckError.value = error?.message || '用户编码不存在'
if (!silent) {
ElMessage.warning(userCodeCheckError.value)
}
return false
} finally {
checkingUserCode.value = false
}
}
const scheduleUserCodeCheck = (value: string) => {
if (userCodeCheckTimer) {
clearTimeout(userCodeCheckTimer)
}
const normalizedValue = value.trim()
if (!normalizedValue) {
resetUserCodeCheckState()
checkingUserCode.value = false
return
}
userCodeCheckTimer = setTimeout(() => {
void runUserCodeCheck(normalizedValue, true)
}, 360)
}
const loadStatisticsData = async () => {
if (!props.selectedProcess || !props.selectedStep) {
resetDashboardData()
return
}
const requestToken = ++statisticsRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const params = {
process_id: processSnapshot,
step_id: stepSnapshot,
}
const [statsRes, userStatsRes] = await Promise.all([
getStatistics(params).catch(() => ({ today_total: 0, success_total: 0, failed_total: 0 })),
getUserCodeStats(params).catch(() => []),
])
if (requestToken !== statisticsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) {
return
}
statistics.value = {
today_total: Number(statsRes?.today_total || 0),
success_total: Number(statsRes?.success_total || 0),
failed_total: Number(statsRes?.failed_total || 0),
}
userStats.value = Array.isArray(userStatsRes) ? userStatsRes : []
}
const loadRecentData = async () => {
if (!props.selectedProcess || !props.selectedStep) {
recentRecords.value = []
return
}
const requestToken = ++recentRecordsRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const res = await getRecentRecords({
process_id: processSnapshot,
step_id: stepSnapshot,
limit: 10,
}).catch(() => ({ items: [] }))
if (requestToken !== recentRecordsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) {
return
}
recentRecords.value = Array.isArray(res?.items) ? res.items : []
}
const loadDataEntryConfig = async () => {
const requestToken = ++dataEntryConfigRequestToken
const processSnapshot = props.selectedProcess
const stepSnapshot = props.selectedStep
const orderSnapshot = orderNo.value.trim()
const res = await getDataEntryConfig({
process_id: processSnapshot,
step_id: stepSnapshot,
apply_mode: 'order',
order_no: orderSnapshot,
}).catch(() => null)
if (
requestToken !== dataEntryConfigRequestToken
|| !isSameSelection(processSnapshot, stepSnapshot)
|| orderNo.value.trim() !== orderSnapshot
) {
return
}
basicSettings.value = res?.basic_setting || {}
specificRules.value = Array.isArray(res?.specific_rules) ? res.specific_rules : []
}
const matchSpecificRule = (rule: ProduceDataEntrySpecificRule, valueMap: Record<string, string>) => {
const matchField = rule.match_field || 'order_no'
const matchValue = String(valueMap[matchField] || '').trim()
const pattern = String(rule.rule_pattern || '').trim()
if (!matchValue || !pattern) {
return false
}
try {
return new RegExp(pattern).test(matchValue)
} catch {
return false
}
}
const validateSpecificRules = () => {
if (!specificRules.value.length) {
return true
}
const context = {
order_no: orderNo.value.trim(),
user_code: userCode.value.trim(),
}
const blockedRule = specificRules.value.find((rule: ProduceDataEntrySpecificRule) => {
if ((rule.apply_mode || 'order') !== 'order' && rule.apply_mode !== 'both') {
return false
}
return Number(rule.rule_type) === 2 && matchSpecificRule(rule, context)
})
if (blockedRule) {
ElMessage.warning(blockedRule.rule_name || '当前订单命中特定规则,禁止提交')
focusPrimaryInput()
return false
}
return true
}
const validateBeforeSubmit = () => {
if (!props.selectedProcess) {
ElMessage.warning('请选择工序')
return false
}
if (!props.selectedStep) {
ElMessage.warning('请选择步骤')
return false
}
if (!orderNo.value.trim()) {
ElMessage.warning('请输入订单号')
focusPrimaryInput()
return false
}
const orderNoMinLength = Number(basicSettings.value.order_no_min_length || 0)
if (orderNoMinLength > 0 && orderNo.value.trim().length < orderNoMinLength) {
ElMessage.warning(`订单号长度不能少于 ${orderNoMinLength} 位`)
focusPrimaryInput()
return false
}
const orderNoRegex = String(basicSettings.value.order_no_regex || '').trim()
if (orderNoRegex) {
try {
const regex = new RegExp(orderNoRegex)
if (!regex.test(orderNo.value.trim())) {
ElMessage.warning('订单号格式不符合基础设置规则')
focusPrimaryInput()
return false
}
} catch {
}
}
if (!userCode.value.trim()) {
ElMessage.warning('请输入用户编码')
focusUserCodeInput()
return false
}
const userCodeRegex = String(basicSettings.value.user_code_regex || '').trim()
if (userCodeRegex) {
try {
const regex = new RegExp(userCodeRegex)
if (!regex.test(userCode.value.trim())) {
ElMessage.warning('用户编码格式不符合基础设置规则')
focusUserCodeInput()
return false
}
} catch {
}
}
if (userCodeCheckError.value) {
ElMessage.warning(userCodeCheckError.value)
focusUserCodeInput()
return false
}
if (!validateSpecificRules()) {
return false
}
return true
}
const resetForm = () => {
orderNo.value = ''
if (!autoUserCode.value) {
userCode.value = ''
resetUserCodeCheckState()
}
saveLocalState()
focusPrimaryInput()
}
const submitEntry = async () => {
if (!validateBeforeSubmit()) return
const passed = await runUserCodeCheck(userCode.value)
if (!passed) {
emit('play-error')
nextTick(() => focusUserCodeInput())
return
}
submitting.value = true
try {
const result = await submitScanData({
order_no: orderNo.value.trim(),
process_id: props.selectedProcess,
step_id: props.selectedStep,
user_code: userCode.value.trim(),
scan_result: 1,
})
if (Number(result?.code) !== 0) {
throw new Error(result?.msg || '录入失败')
}
ElMessage.success('录入成功')
emit('play-success')
emit('speak-success')
saveLocalState()
resetForm()
await Promise.all([loadStatisticsData(), loadRecentData()])
} catch (error: any) {
ElMessage.error(error?.message || '录入失败')
emit('play-error')
emit('speak-error')
} finally {
submitting.value = false
}
}
const refreshBySelection = async () => {
if (!props.selectedProcess || !props.selectedStep) {
basicSettings.value = {}
specificRules.value = []
resetDashboardData()
return
}
resetDashboardData()
await Promise.all([loadStatisticsData(), loadRecentData(), loadDataEntryConfig()])
focusPrimaryInput()
}
watch(
() => [props.selectedProcess, props.selectedStep],
() => {
if (!initialized) return
void refreshBySelection()
}
)
watch(orderNo, () => {
if (!initialized) return
saveLocalState()
void loadDataEntryConfig()
})
watch(userCode, () => {
if (!initialized) return
scheduleUserCodeCheck(userCode.value)
if (autoUserCode.value || userCode.value.trim()) {
saveLocalState()
}
})
watch([autoUserCode, hideProcessTotal], () => {
if (!initialized) return
saveLocalState()
})
onMounted(async () => {
restoreLocalState()
await refreshBySelection()
initialized = true
})
onBeforeUnmount(() => {
if (userCodeCheckTimer) {
clearTimeout(userCodeCheckTimer)
}
clearScannerSubmitTimer()
})
defineExpose({
focusPrimaryInput,
focusUserCodeInput,
submitEntry,
resetForm,
})
</script>
<style scoped src="./entry-panel.css"></style>
@@ -0,0 +1,340 @@
.main-top-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(340px, 1fr);
gap: 20px;
align-items: stretch;
}
.workbench-panel,
.records-panel {
background: #ffffff;
border-radius: 20px;
border: 1px solid #e5e7eb;
padding: 28px;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
}
.column-header {
padding-bottom: 18px;
border-bottom: 1px solid #eef2f7;
margin-bottom: 22px;
}
.panel-metric-card {
min-width: 240px;
min-height: 56px;
padding: 12px 16px;
border-radius: 16px;
background: #f8fbff;
border: 1px solid #e3ebf7;
display: flex;
align-items: center;
gap: 12px;
}
.panel-metric-label {
font-size: 13px;
color: #64748b;
}
.panel-metric-value {
font-size: 28px;
line-height: 1;
font-weight: 700;
color: #2563eb;
margin-left: auto;
}
.metric-eye-button {
width: 32px;
height: 32px;
border: 0;
border-radius: 999px;
background: transparent;
display: inline-flex;
align-items: center;
justify-content: center;
color: #94a3b8;
cursor: pointer;
transition: all 0.2s ease;
}
.metric-eye-button svg {
width: 18px;
height: 18px;
}
.metric-eye-button.is-hidden {
color: #c0cad8;
}
.metric-eye-button:hover {
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
}
.panel-title {
font-size: 18px;
font-weight: 600;
color: #2563eb;
}
.panel-subtitle {
margin-top: 8px;
font-size: 14px;
color: #6b7280;
}
.workbench-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.field-row {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.field-row-split {
grid-template-columns: minmax(0, 1fr) 180px;
}
.field-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.field-group-quantity {
min-width: 0;
}
.field-group label {
font-size: 14px;
font-weight: 600;
color: #334155;
}
.field-error-tip {
font-size: 12px;
color: #ef4444;
margin-top: 6px;
}
.field-primary :deep(.el-input__wrapper),
.field-row :deep(.el-input__wrapper),
.field-row :deep(.el-input-number .el-input__wrapper) {
min-height: 58px;
border-radius: 14px;
box-shadow: 0 0 0 1px #dbe4f0 inset;
}
.field-primary :deep(.el-input__inner),
.field-row :deep(.el-input__inner),
.field-row :deep(.el-input-number .el-input__inner) {
font-size: 18px;
}
.field-primary :deep(.el-input__wrapper.is-focus),
.field-row :deep(.el-input__wrapper.is-focus),
.field-row :deep(.el-input-number .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
.quantity-input {
width: 100%;
}
.quantity-input :deep(.el-input__wrapper) {
padding-right: 40px;
overflow: hidden;
}
.quantity-input :deep(.el-input-number__increase),
.quantity-input :deep(.el-input-number__decrease) {
right: 1px;
width: 36px;
margin: 0;
background: #f8fafc;
color: #94a3b8;
border-left: 1px solid #dbe4f0;
box-shadow: none;
}
.quantity-input :deep(.el-input-number__increase) {
top: 1px;
height: calc(50% - 0.5px);
border-top-right-radius: 13px;
border-bottom: 1px solid #e5edf6;
}
.quantity-input :deep(.el-input-number__decrease) {
bottom: 1px;
height: calc(50% - 0.5px);
border-bottom-right-radius: 13px;
}
.quantity-input :deep(.el-input-number__increase:hover),
.quantity-input :deep(.el-input-number__decrease:hover) {
color: var(--el-color-primary);
background: #f1f5f9;
}
.field-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.lock-switch {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #64748b;
}
.action-row {
display: flex;
gap: 12px;
}
.action-row :deep(.el-button) {
min-width: 160px;
border-radius: 12px;
}
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.dashboard-title {
font-size: 18px;
font-weight: 600;
color: #111827;
}
.dashboard-caption {
font-size: 13px;
color: #94a3b8;
}
.station-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.station-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 14px;
background: #ffffff;
border: 1px solid #e6edf6;
}
.station-icon {
width: 34px;
height: 34px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #e8f1ff;
color: #3b82f6;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.station-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.station-info strong {
font-size: 16px;
color: #1f2937;
}
.station-card b {
margin-left: auto;
font-size: 32px;
line-height: 1;
color: #2563eb;
}
.records-header {
margin-bottom: 20px;
}
.records-panel :deep(.el-table) {
--el-table-border-color: #eef2f7;
--el-table-header-bg-color: #f8fafc;
}
.records-panel :deep(.el-table th.el-table__cell) {
font-weight: 600;
color: #475569;
}
.records-panel :deep(.el-table td.el-table__cell) {
padding-top: 14px;
padding-bottom: 14px;
}
@media (max-width: 1024px) {
.panel-header,
.field-label-row,
.dashboard-header {
flex-direction: column;
align-items: flex-start;
}
.panel-metric-card {
width: 100%;
min-width: 0;
}
.main-top-grid,
.station-grid {
grid-template-columns: 1fr;
}
.field-row-split {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.workbench-panel,
.records-panel {
padding: 18px;
border-radius: 16px;
}
.action-row {
flex-wrap: wrap;
}
.action-row :deep(.el-button) {
width: 100%;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
<template>
<div class="produce-work-page exception-page">
<div class="page-shell">
<section class="page-hero exception-hero">
<div>
<span class="eyebrow">现场提报</span>
<h1>异常处理</h1>
<p>先校验订单,再选择异常类型与原因,补充现场信息后提交。</p>
</div>
<div class="hero-user">
<strong>{{ currentUser.name || '未登录' }}</strong>
<span>{{ currentUser.code || '请先登录' }}</span>
</div>
</section>
<section class="summary-grid">
<div v-for="item in summaryItems" :key="item.label" class="summary-card">
<strong :class="item.type">{{ item.value }}</strong>
<span>{{ item.label }}</span>
</div>
</section>
<div class="content-grid">
<main class="main-column">
<section class="panel">
<div class="panel-title"><span>1</span><div><h2>扫描或录入单号</h2><p>支持订单号、整机编码或设备条码</p></div></div>
<div class="inline-form">
<el-input v-model="form.orderNo" size="large" clearable placeholder="请输入单号 / 条码" @keyup.enter="checkOrder" />
<el-button type="primary" size="large" :loading="checking" @click="checkOrder">校验单号</el-button>
</div>
<div v-if="orderMessage" class="validation" :class="orderValid ? 'success' : 'error'">{{ orderMessage }}</div>
</section>
<section class="panel">
<div class="panel-title"><span>2</span><div><h2>选择处理类型</h2><p>根据现场异常选择对应的处理分类</p></div><em>必选</em></div>
<div class="option-grid type-grid">
<button v-for="item in typeOptions" :key="item.value" type="button" :class="['choice-card', { active: form.type === item.value }]" @click="selectType(item.value)">
<strong>{{ item.label }}</strong><small>{{ item.desc || '按异常设置执行处理' }}</small>
</button>
</div>
<div class="sub-title"><h2>3. 选择异常原因</h2><span>快速点选</span></div>
<div class="option-grid reason-grid">
<button v-for="item in activeReasons" :key="item.value" type="button" :class="['reason-card', { active: form.reason === item.value }]" @click="selectReason(item.value)">{{ item.label }}</button>
</div>
<div v-if="activeIssues.length" class="issue-box">
<div class="sub-title"><h2>具体问题</h2><span>{{ activeReasons.find(item => item.value === form.reason)?.label }}</span></div>
<el-select v-model="form.issue" filterable clearable class="full-width" placeholder="请选择具体问题">
<el-option v-for="item in activeIssues" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</section>
<section class="panel">
<div class="panel-title"><span>4</span><div><h2>现场取证</h2><p>最多上传 4 张现场照片</p></div><em class="optional">选填</em></div>
<el-upload v-model:file-list="imageFiles" list-type="picture-card" :auto-upload="false" :limit="4" accept="image/*" :on-exceed="handleImageExceed">
<span class="upload-plus">+</span>
</el-upload>
<el-input v-model="form.remark" class="remark-input" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="补充现场情况、处理建议、责任工位等" />
</section>
<div class="form-actions">
<el-button size="large" @click="resetForm">重置</el-button>
<el-button type="primary" size="large" :loading="submitting" @click="submit">提交异常</el-button>
</div>
</main>
<aside class="side-column">
<section class="panel operator-panel"><h2>提报信息</h2><div><span>提报人</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>提交时间</span><strong>{{ nowText }}</strong></div></section>
<section class="panel"><div class="side-title"><h2>最近异常</h2><span>现场回看</span></div><el-empty v-if="!recentExceptions.length" description="暂无异常记录" :image-size="70" /><div v-for="item in recentExceptions" :key="item.id" class="recent-item"><div><strong>{{ item.order_no || '-' }}</strong><p>{{ item.type_name || '-' }} · {{ [item.reason_name, item.issue_name].filter(Boolean).join(' · ') }}</p></div><el-tag :type="item.status === 2 ? 'success' : item.status === 1 ? 'warning' : 'danger'">{{ item.status_name || '待处理' }}</el-tag></div></section>
</aside>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import type { UploadUserFile } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { useMemberStore } from '~/stores/member'
import { checkExceptionOrder, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord, uploadProduceImage, type ProduceExceptionRecord, type ProduceExceptionSetting } from '~/apps/produce/api/exception'
const memberStore = useMemberStore()
const currentUser = computed(() => ({ name: String(memberStore.info?.nickname || memberStore.info?.real_name || memberStore.info?.username || ''), code: String(memberStore.info?.username || memberStore.info?.user_code || '') }))
const form = ref({ orderNo: '', type: '', reason: '', issue: '', remark: '' })
const settings = ref<ProduceExceptionSetting[]>([])
const recentExceptions = ref<ProduceExceptionRecord[]>([])
const summary = ref({ pending: 0, review: 0, done: 0 })
const imageFiles = ref<UploadUserFile[]>([])
const checking = ref(false)
const submitting = ref(false)
const orderValid = ref(false)
const orderMessage = ref('')
const nowText = ref('')
const typeOptions = computed(() => settings.value.filter(item => item.setting_type === 'type').map(toOption))
const activeReasons = computed(() => settings.value.filter(item => item.setting_type === 'reason' && String(item.parent_id || '') === String(form.value.type)).map(toOption))
const activeIssues = computed(() => settings.value.filter(item => item.setting_type === 'issue' && String(item.parent_id || '') === String(form.value.reason)).map(toOption))
const summaryItems = computed(() => [{ label: '待处理', value: summary.value.pending, type: 'danger' }, { label: '待复核', value: summary.value.review, type: 'warning' }, { label: '已处理', value: summary.value.done, type: 'success' }])
const toOption = (item: ProduceExceptionSetting) => ({ value: String(item.id), label: item.name, desc: item.remark || '' })
const updateNow = () => { nowText.value = new Date().toLocaleString('zh-CN', { hour12: false }) }
const loadData = async () => {
try {
const [settingData, statisticData, recentData] = await Promise.all([getExceptionSettings(), getExceptionStatistics({ user_code: currentUser.value.code }), getRecentExceptions({ user_code: currentUser.value.code, limit: 5 })])
settings.value = Array.isArray(settingData) ? settingData : []
summary.value = statisticData || summary.value
recentExceptions.value = Array.isArray(recentData) ? recentData : []
if (!form.value.type) selectType(typeOptions.value[0]?.value || '')
} catch { /* 请求层已提示错误 */ }
}
const selectType = (value: string) => { form.value.type = value; form.value.reason = activeReasons.value[0]?.value || ''; form.value.issue = activeIssues.value[0]?.value || '' }
const selectReason = (value: string) => { form.value.reason = value; form.value.issue = activeIssues.value[0]?.value || '' }
const checkOrder = async () => {
const orderNo = form.value.orderNo.trim()
if (!orderNo) { orderMessage.value = '请输入单号或条码'; orderValid.value = false; return }
checking.value = true
try { const result = await checkExceptionOrder(orderNo); orderValid.value = true; orderMessage.value = `已匹配${result?.reference_name || '记录'}` } catch { orderValid.value = false; orderMessage.value = '未找到对应订单或整机记录' } finally { checking.value = false }
}
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
const resetForm = () => { form.value = { orderNo: '', type: typeOptions.value[0]?.value || '', reason: '', issue: '', remark: '' }; imageFiles.value = []; orderValid.value = false; orderMessage.value = ''; selectType(form.value.type); updateNow() }
const submit = async () => {
if (!orderValid.value || !form.value.orderNo.trim()) { await checkOrder(); if (!orderValid.value) return }
if (!form.value.type || !form.value.reason || (activeIssues.value.length && !form.value.issue)) { ElMessage.warning('请选择处理类型、异常原因和具体问题'); return }
if (!currentUser.value.code) { ElMessage.warning('请先登录后再提报异常'); return }
submitting.value = true
try {
const images: string[] = []
for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'exception'); if (url) images.push(url) }
const response = await submitExceptionRecord({ order_no: form.value.orderNo.trim(), user_code: currentUser.value.code, type_id: form.value.type, reason_id: form.value.reason, issue_id: form.value.issue || '', images, remark: form.value.remark.trim() })
if (response?.code !== 0) { ElMessage.error(response?.msg || '异常提交失败'); return }
ElMessage.success('异常已提交'); resetForm(); await loadData()
} finally { submitting.value = false }
}
onMounted(() => { updateNow(); void loadData() })
</script>
<style scoped>
.produce-work-page { min-height: calc(100vh - 60px); padding: 28px; background: #f5f7fb; color: #17233d; }
.page-shell { max-width: 1440px; margin: 0 auto; }
.page-hero { display:flex; align-items:center; justify-content:space-between; gap:24px; padding:32px 36px; border-radius:22px; color:#fff; box-shadow:0 14px 32px rgba(37,99,235,.12); }
.exception-hero { background:linear-gradient(135deg,#ff9d42,#ef625f); }
.eyebrow { font-size:13px; opacity:.85; } h1 { margin:8px 0 0; font-size:30px; } .page-hero p { margin:8px 0 0; opacity:.86; }
.hero-user { min-width:150px; padding:16px 20px; border-radius:14px; background:rgba(255,255,255,.16); text-align:right; } .hero-user strong,.hero-user span { display:block; } .hero-user span { margin-top:6px; font-size:13px; opacity:.8; }
.summary-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin:20px 0; } .summary-card,.panel { background:#fff; border:1px solid #e7edf5; border-radius:18px; box-shadow:0 8px 24px rgba(36,59,92,.04); } .summary-card { padding:18px 22px; } .summary-card strong,.summary-card span { display:block; } .summary-card strong { font-size:28px; } .summary-card span { margin-top:6px; color:#8a94a6; font-size:13px; } .danger { color:#ef625f; } .warning { color:#e59a2f; } .success { color:#26b36c; }
.content-grid { display:grid; grid-template-columns:minmax(0,1fr) 350px; gap:20px; align-items:start; } .main-column,.side-column { display:flex; flex-direction:column; gap:20px; } .panel { padding:26px; } .panel-title,.side-title,.sub-title { display:flex; align-items:flex-start; gap:14px; } .panel-title > span { display:flex; align-items:center; justify-content:center; width:30px; height:30px; border-radius:50%; background:#fff0e5; color:#e97020; font-weight:700; flex:none; } h2 { margin:0; font-size:17px; } .panel-title p { margin:6px 0 0; color:#9aa4b4; font-size:13px; } .panel-title em { margin-left:auto; padding:5px 10px; border-radius:999px; color:#e97020; background:#fff3e8; font-size:12px; font-style:normal; } .panel-title em.optional { color:#8793a6; background:#f3f5f8; }
.inline-form { display:flex; gap:12px; margin-top:20px; } .inline-form .el-input { flex:1; } .validation { margin-top:10px; font-size:13px; } .validation.success { color:#26b36c; } .validation.error { color:#ef625f; } .option-grid { display:grid; gap:12px; margin-top:18px; } .type-grid { grid-template-columns:repeat(2,1fr); } button { font:inherit; cursor:pointer; } .choice-card,.reason-card { border:1px solid #e6edf5; border-radius:14px; background:#f8fafc; text-align:left; transition:.2s; } .choice-card { padding:16px; } .choice-card strong,.choice-card small { display:block; } .choice-card strong { color:#2b3853; } .choice-card small { margin-top:8px; color:#9aa4b4; } .choice-card.active,.reason-card.active { border-color:#ff9a3c; background:#fff5ed; color:#e97020; } .sub-title { align-items:center; justify-content:space-between; margin-top:26px; } .sub-title span,.side-title span { color:#9aa4b4; font-size:12px; } .reason-grid { grid-template-columns:repeat(3,1fr); } .reason-card { min-height:46px; padding:8px; text-align:center; color:#56647a; } .issue-box { margin-top:18px; padding:16px; border-radius:14px; background:#fffaf5; } .issue-box .sub-title { margin:0 0 12px; } .full-width { width:100%; }
:deep(.el-upload--picture-card),:deep(.el-upload-list__item) { width:92px; height:92px; } .upload-plus { font-size:30px; color:#ff9a3c; } .remark-input { margin-top:18px; } .form-actions { display:flex; justify-content:flex-end; gap:12px; } .operator-panel h2 { margin-bottom:18px; } .operator-panel div { display:flex; justify-content:space-between; padding:13px 0; border-bottom:1px solid #eef2f7; font-size:13px; } .operator-panel div:last-child { border-bottom:0; } .operator-panel span { color:#9aa4b4; } .side-title { align-items:center; justify-content:space-between; } .recent-item { display:flex; justify-content:space-between; gap:10px; padding:16px 0; border-bottom:1px solid #eef2f7; } .recent-item:last-child { border-bottom:0; } .recent-item strong { font-size:14px; } .recent-item p { margin:6px 0 0; color:#8a94a6; font-size:12px; }
@media (max-width: 900px) { .produce-work-page { padding:16px; } .content-grid { grid-template-columns:1fr; } .side-column { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); } } @media (max-width: 640px) { .page-hero { padding:24px; align-items:flex-start; flex-direction:column; } .hero-user { align-self:stretch; text-align:left; } .summary-grid,.side-column { grid-template-columns:1fr; } .type-grid,.reason-grid { grid-template-columns:1fr; } .inline-form { flex-direction:column; } .form-actions { justify-content:stretch; } .form-actions .el-button { flex:1; } }
</style>
@@ -0,0 +1,179 @@
<template>
<div class="produce-query-page">
<div class="page-shell">
<section class="page-hero">
<div>
<span class="eyebrow">生产追溯中心</span>
<h1>综合查询工作台</h1>
<p>按订单号查看生产、异常和维修全过程;也可以独立查询各类业务记录。</p>
</div>
<div class="hero-badge"><strong>{{ timeline.total }}</strong><span>订单时间线记录</span></div>
</section>
<section class="panel timeline-panel">
<div class="panel-heading"><div><h2>订单全流程时间线</h2><p>订单号查询会汇总扫码、整机、异常和维修记录,并按发生时间排序。</p></div></div>
<div class="search-row">
<el-input v-model="timelineOrderNo" size="large" clearable placeholder="请输入订单号" @keyup.enter="loadTimeline">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-button type="primary" size="large" :loading="timelineLoading" @click="loadTimeline">查询时间线</el-button>
</div>
<el-alert v-if="timelineMessage" :title="timelineMessage" :type="timeline.total ? 'success' : 'info'" :closable="false" class="timeline-alert" />
<el-empty v-if="timelineLoaded && !timelineLoading && !timeline.items.length" description="该订单暂无相关记录" :image-size="90" />
<div v-else-if="timeline.items.length" class="timeline-list">
<el-table :data="timeline.items" stripe class="timeline-table" empty-text="暂无时间线记录">
<el-table-column prop="event_date" label="发生时间" width="170" />
<el-table-column label="记录类型" width="112">
<template #default="scope"><el-tag :type="tagType(scope.row.record_type)" effect="light" round>{{ scope.row.record_type_name }}</el-tag></template>
</el-table-column>
<el-table-column label="订单 / 产品" min-width="220" show-overflow-tooltip>
<template #default="scope">
<div class="timeline-content-cell">
<span class="content-mark" aria-hidden="true" />
<div class="timeline-main-text">
<strong>{{ scope.row.order_no || timeline.order_no || '-' }}</strong>
<small v-if="scope.row.product_code">产品:{{ scope.row.product_code }}</small>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="工序节点" min-width="160" show-overflow-tooltip>
<template #default="scope">
<div class="timeline-node-cell">
<strong>{{ scope.row.process_name || '未指定工序' }}</strong>
<small v-if="scope.row.step_name">工位:{{ scope.row.step_name }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="状态 / 结果" min-width="155" show-overflow-tooltip>
<template #default="scope">
<span class="timeline-status">{{ [scope.row.status_name, scope.row.result_name].filter(Boolean).join(' · ') || '已记录' }}</span>
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="78" align="right">
<template #default="scope">{{ scope.row.quantity ?? '—' }}</template>
</el-table-column>
<el-table-column label="业务详情" min-width="280" show-overflow-tooltip>
<template #default="scope">
<div v-if="scope.row.record_type === 'exception'" class="timeline-detail-cell">
<span><b>异常原因</b>{{ scope.row.exception_reason || '未填写' }}</span>
<span><b>具体问题</b>{{ scope.row.exception_content || '未填写' }}</span>
</div>
<div v-else-if="scope.row.record_type === 'repair'" class="timeline-detail-cell">
<span><b>物料分类</b>{{ scope.row.material_category || '未填写' }}</span>
<span><b>故障描述</b>{{ scope.row.fault_description || '未填写' }}</span>
</div>
<span v-else class="muted-placeholder">—</span>
</template>
</el-table-column>
<el-table-column label="操作人" min-width="130" show-overflow-tooltip>
<template #default="scope">
<div class="timeline-operator-cell">
<span>{{ scope.row.operator || '未记录' }}</span>
<small v-if="scope.row.user_code">工号:{{ scope.row.user_code }}</small>
</div>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
</el-table>
</div>
</section>
<section class="panel independent-panel">
<div class="panel-heading"><div><h2>独立记录查询</h2><p>切换记录类型,按条件分页查询。</p></div><el-button @click="resetIndependent">重置</el-button></div>
<el-tabs v-model="filters.record_type" class="query-tabs" @tab-change="handleRecordTypeChange">
<el-tab-pane label="扫码记录" name="scan" />
<el-tab-pane label="整机记录" name="product" />
<el-tab-pane label="异常记录" name="exception" />
<el-tab-pane label="维修记录" name="repair" />
</el-tabs>
<el-form :inline="true" class="filter-form" @submit.prevent="loadRecords">
<el-form-item label="关键词"><el-input v-model="filters.keyword" clearable :placeholder="keywordPlaceholder" /></el-form-item>
<el-form-item label="订单号"><el-input v-model="filters.order_no" clearable placeholder="精确订单号" /></el-form-item>
<el-form-item v-if="filters.record_type !== 'repair'" label="用户工号"><el-input v-model="filters.user_code" clearable placeholder="用户工号" /></el-form-item>
<el-form-item label="日期范围"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
<el-form-item><el-button type="primary" :loading="recordLoading" @click="loadRecords">查询</el-button></el-form-item>
</el-form>
<el-table v-loading="recordLoading" :data="recordPage.items" stripe class="record-table" empty-text="暂无查询记录" @row-dblclick="showRecordDetail">
<el-table-column prop="event_date" label="发生时间" width="170" />
<el-table-column prop="record_type_name" label="记录类型" width="112"><template #default="scope"><el-tag :type="tagType(scope.row.record_type)" effect="light" round>{{ scope.row.record_type_name }}</el-tag></template></el-table-column>
<el-table-column prop="order_no" label="订单号" min-width="160" show-overflow-tooltip />
<el-table-column prop="product_code" label="产品编码" min-width="145" show-overflow-tooltip />
<el-table-column label="工序节点" min-width="160" show-overflow-tooltip><template #default="scope">{{ [scope.row.process_name, scope.row.step_name].filter(Boolean).join(' / ') || '—' }}</template></el-table-column>
<el-table-column label="状态 / 结果" min-width="145" show-overflow-tooltip><template #default="scope">{{ [scope.row.status_name, scope.row.result_name].filter(Boolean).join(' · ') || '—' }}</template></el-table-column>
<el-table-column label="操作人" min-width="125" show-overflow-tooltip><template #default="scope">{{ scope.row.operator || scope.row.user_code || '未记录' }}</template></el-table-column>
<el-table-column prop="remark" label="备注" min-width="190" show-overflow-tooltip />
</el-table>
<div class="pagination-row"><span>共 {{ recordPage.total }} 条</span><el-pagination v-model:current-page="recordPage.page" v-model:page-size="recordPage.limit" layout="prev, pager, next, sizes" :page-sizes="[20, 50, 100]" :total="recordPage.total" @current-change="loadRecords" @size-change="loadRecords" /></div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, onMounted, reactive, ref } from 'vue'
import { queryProduceRecords, queryProduceTimeline, type ProduceQueryRecord, type ProduceQueryRecordType } from '~/apps/produce/api/query'
const timelineOrderNo = ref('')
const timelineLoading = ref(false)
const timelineLoaded = ref(false)
const timelineMessage = ref('')
const timeline = reactive<{ order_no: string; total: number; items: ProduceQueryRecord[] }>({ order_no: '', total: 0, items: [] })
const filters = reactive<{ record_type: ProduceQueryRecordType; keyword: string; order_no: string; user_code: string; start_date?: string; end_date?: string }>({ record_type: 'scan', keyword: '', order_no: '', user_code: '' })
const dateRange = ref<string[]>([])
const recordLoading = ref(false)
const recordPage = reactive({ items: [] as ProduceQueryRecord[], total: 0, page: 1, limit: 20 })
const tagTypes: Record<ProduceQueryRecordType, 'primary' | 'success' | 'danger' | 'warning'> = { scan: 'primary', product: 'success', exception: 'danger', repair: 'warning' }
const tagType = (type: ProduceQueryRecordType) => tagTypes[type]
const keywordPlaceholder = computed(() => filters.record_type === 'repair' ? '维修单、产品、人员或备注' : '订单、产品、人员或备注')
const handleRecordTypeChange = () => { recordPage.page = 1; void loadRecords() }
const showRecordDetail = (row: ProduceQueryRecord) => { ElMessage.info(`${row.record_type_name}详情功能待接入`) }
const loadTimeline = async () => { const orderNo = timelineOrderNo.value.trim(); if (!orderNo) return ElMessage.warning('请输入订单号'); timelineLoading.value = true; timelineLoaded.value = false; timelineMessage.value = ''; try { const data = await queryProduceTimeline(orderNo); timeline.order_no = data?.order_no || orderNo; timeline.total = Number(data?.total || 0); timeline.items = Array.isArray(data?.items) ? data.items : []; timelineMessage.value = timeline.total ? `已找到 ${timeline.total} 条全流程记录` : '该订单暂无相关记录'; timelineLoaded.value = true } catch { timeline.order_no = orderNo; timeline.total = 0; timeline.items = []; timelineMessage.value = '订单查询失败,请检查订单号后重试'; timelineLoaded.value = true } finally { timelineLoading.value = false } }
const loadRecords = async () => { recordLoading.value = true; try { const data = await queryProduceRecords({ ...filters, ...(dateRange.value.length ? { start_date: dateRange.value[0], end_date: dateRange.value[1] } : {}), page: recordPage.page, limit: recordPage.limit }); recordPage.items = Array.isArray(data?.items) ? data.items : []; recordPage.total = Number(data?.total || 0) } catch { recordPage.items = []; recordPage.total = 0; ElMessage.error('记录查询失败,请稍后重试') } finally { recordLoading.value = false } }
const resetIndependent = () => { filters.record_type = 'scan'; filters.keyword = ''; filters.order_no = ''; filters.user_code = ''; dateRange.value = []; recordPage.page = 1; void loadRecords() }
onMounted(() => { void loadRecords() })
</script>
<style scoped>
.produce-query-page { min-height: calc(100vh - 60px); padding: 28px; color: #17233d; background: #f5f7fb; }
.page-shell { max-width: 1440px; margin: 0 auto; }
.page-hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 32px 36px; color: #fff; background: linear-gradient(135deg, #315efb, #6a67df); border-radius: 22px; box-shadow: 0 14px 32px rgba(49, 94, 251, .16); }
.eyebrow { font-size: 13px; opacity: .85; }
.page-hero h1 { margin: 8px 0 0; font-size: 30px; }
.page-hero p { margin: 8px 0 0; opacity: .86; }
.hero-badge { min-width: 150px; padding: 16px 20px; text-align: right; background: rgba(255, 255, 255, .16); border-radius: 14px; }
.hero-badge strong, .hero-badge span { display: block; }
.hero-badge strong { font-size: 28px; }
.hero-badge span { margin-top: 5px; font-size: 13px; opacity: .82; }
.panel { margin-top: 20px; padding: 26px; background: #fff; border: 1px solid #e7edf5; border-radius: 18px; box-shadow: 0 8px 24px rgba(36, 59, 92, .04); }
.panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.panel-heading h2 { margin: 0; font-size: 18px; }
.panel-heading p { margin: 6px 0 0; color: #8a94a6; font-size: 13px; }
.search-row { display: flex; gap: 12px; }
.search-row .el-input { flex: 1; }
.timeline-alert { margin-top: 16px; }
.timeline-list { margin: 24px 0 0; overflow-x: auto; }
.timeline-table, .record-table { min-width: 1100px; overflow: hidden; border: 1px solid #e7edf5; border-radius: 12px; }
.timeline-table :deep(.el-table__header th), .record-table :deep(.el-table__header th) { height: 48px; color: #5d6b82; font-size: 13px; font-weight: 600; background: #f7f9fc; }
.timeline-table :deep(.el-table__cell), .record-table :deep(.el-table__cell) { padding: 12px 0; color: #34425b; font-size: 13px; }
.timeline-table :deep(.el-table__row:hover > td), .record-table :deep(.el-table__row:hover > td) { background: #f4f7ff !important; }
.timeline-content-cell, .timeline-node-cell { display: flex; align-items: center; min-width: 0; }
.timeline-content-cell { gap: 10px; }
.timeline-main-text, .timeline-node-cell, .timeline-operator-cell { min-width: 0; }
.timeline-content-cell strong, .timeline-node-cell strong { display: block; overflow: hidden; color: #24324b; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.timeline-content-cell small, .timeline-node-cell small, .timeline-operator-cell small { display: block; margin-top: 4px; overflow: hidden; color: #8a94a6; font-size: 12px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
.content-mark { width: 8px; height: 8px; flex: none; background: #315efb; border-radius: 50%; box-shadow: 0 0 0 4px #e5ebff; }
.timeline-status { color: #3157c8; font-weight: 500; }
.timeline-detail-cell { display: grid; gap: 5px; min-width: 0; line-height: 1.35; }
.timeline-detail-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.timeline-detail-cell b { display: inline-block; min-width: 64px; margin-right: 6px; color: #8a94a6; font-size: 12px; font-weight: 500; }
.muted-placeholder { color: #b5bdca; }
.filter-form { display: flex; flex-wrap: wrap; margin-bottom: 4px; }
.record-table { margin-top: 14px; }
.pagination-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; color: #8a94a6; font-size: 13px; }
.pagination-row .el-pagination { margin-left: auto; }
@media (max-width: 640px) { .produce-query-page { padding: 16px; } .page-hero { align-items: flex-start; flex-direction: column; padding: 24px; } .hero-badge { align-self: stretch; text-align: left; } .search-row { flex-direction: column; } .panel { padding: 18px; } .pagination-row { align-items: flex-start; flex-direction: column; } .pagination-row .el-pagination { margin-left: 0; } }
</style>
@@ -0,0 +1,105 @@
<template>
<div class="produce-work-page repair-page">
<div class="page-shell">
<section class="page-hero repair-hero">
<div><span class="eyebrow">维修工作台</span><h1>维修处理</h1><p>扫描设备,快速完成故障判断、备件登记与维修记录。</p></div>
<el-icon :size="44"><Tools /></el-icon>
</section>
<section class="summary-grid">
<div v-for="item in summaries" :key="item.label" class="summary-card"><strong :class="item.type">{{ item.value }}</strong><span>{{ item.label }}</span><el-button v-if="item.action" link type="primary" @click="goTasks">查看任务</el-button></div>
</section>
<div class="content-grid">
<main class="main-column">
<section class="panel">
<div class="panel-title"><span>1</span><div><h2>扫描或录入设备</h2><p>支持订单号、整机编码或设备条码</p></div><em>必填</em></div>
<div class="inline-form"><el-input v-model="deviceNo" size="large" clearable placeholder="请输入订单号 / 整机编码" @keyup.enter="loadTask" /><el-button type="primary" size="large" :loading="loadingTask" @click="loadTask">查询任务</el-button></div>
<div v-if="task" class="task-found"><el-icon><CircleCheckFilled /></el-icon><div><strong>已识别维修对象</strong><span>{{ task.device_no }}</span></div><el-tag type="warning">{{ task.status_name || '待维修' }}</el-tag></div>
</section>
<section class="panel">
<div class="panel-title"><span>2</span><div><h2>异常信息</h2><p>来自异常提报的维修任务</p></div><small>{{ task?.reported_date || '待查询设备' }}</small></div>
<el-empty v-if="!task" description="查询设备后加载异常任务" :image-size="72" />
<div v-else class="info-grid"><div><span>异常类型</span><strong>{{ task.type_name || '-' }}</strong></div><div><span>异常原因</span><strong>{{ reasonText }}</strong></div><div><span>提报人员</span><strong>{{ task.reporter || task.user_code || '-' }}</strong></div></div>
</section>
<section class="panel">
<div class="panel-title"><span>3</span><div><h2>维修判定</h2><p>选择本次维修的不良类型</p></div><em>必选</em></div>
<div class="result-grid"><button v-for="item in resultOptions" :key="item.value" type="button" :class="['result-card', { active: form.repair_result === item.value }]" @click="selectResult(item.value)"><el-icon :size="26"><component :is="item.icon" /></el-icon><strong>{{ item.label }}</strong><small>{{ item.desc }}</small></button></div>
</section>
<section v-if="form.repair_result === 'assembly_mismatch'" class="panel"><div class="panel-title"><span>4</span><div><h2>搭配错误分类</h2><p>选择与本次异常对应的物料分类</p></div><em>必选</em></div><el-checkbox-group v-model="form.category_ids" class="category-grid"><el-checkbox v-for="item in categories" :key="item.id" :label="String(item.id)">{{ item.category_name }}</el-checkbox></el-checkbox-group><el-empty v-if="!categories.length" description="暂无可用物料分类" :image-size="60" /></section>
<section v-if="form.repair_result === 'operation_defect'" class="panel"><div class="panel-title"><span>4</span><div><h2>作业不良责任</h2><p>选择责任人员,工序步骤随人员记录保存</p></div><em>必选</em></div><el-checkbox-group v-model="form.responsible_ids" class="responsibility-grid"><el-checkbox v-for="item in scanRecords" :key="item.id" :label="String(item.id)"><span>{{ item.operator || item.user_code || '未记录人员' }}</span><small>{{ item.process_name || '-' }} · {{ item.step_name || '-' }}</small></el-checkbox></el-checkbox-group><el-empty v-if="!scanRecords.length" description="当前订单暂无可定责的历史扫码记录" :image-size="60" /></section>
<section class="panel"><div class="panel-title"><span>{{ form.repair_result === 'material_defect' ? '4' : '5' }}</span><div><h2>维修记录</h2><p>登记更换备件和处理说明,便于后续追溯</p></div></div><div class="part-toolbar"><strong>更换备件</strong><div><el-button size="small" @click="openPartDialog">输入商品编码</el-button><el-button size="small" type="primary" @click="openPartDialog">添加备件</el-button></div></div><el-empty v-if="!form.parts.length" description="暂未添加备件" :image-size="60" /><div v-for="(part, index) in form.parts" :key="part.id" class="part-row"><div class="part-index">{{ index + 1 }}</div><div class="part-info"><strong>{{ part.product_name || part.product_code }}</strong><span>{{ part.product_code }}{{ part.category_name ? ` · ${part.category_name}` : '' }}</span><el-radio-group v-model="part.source" size="small"><el-radio-button label="new">新料</el-radio-button><el-radio-button label="after_sales">售后</el-radio-button><el-radio-button label="refurbished">返修</el-radio-button></el-radio-group><el-select v-model="part.fault_description_ids" multiple collapse-tags filterable clearable placeholder="选择故障点" class="fault-select"><el-option v-for="fault in faultsForPart(part)" :key="fault.id" :label="fault.description" :value="String(fault.id)" /></el-select></div><el-input-number v-model="part.quantity" :min="1" size="small" /><el-button link type="danger" @click="removePart(index)">删除</el-button></div><el-input v-model="form.remark" class="remark-input" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="记录维修过程、处理建议或补充说明" /></section>
<section class="panel"><div class="panel-title"><span>{{ form.repair_result === 'material_defect' ? '5' : '6' }}</span><div><h2>维修凭证</h2><p>可上传维修完成后的现场照片</p></div><em class="optional">选填</em></div><el-upload v-model:file-list="imageFiles" list-type="picture-card" :auto-upload="false" :limit="4" accept="image/*" :on-exceed="handleImageExceed"><span class="upload-plus">+</span></el-upload></section>
<div class="form-actions"><el-button size="large" @click="resetForm">重置</el-button><el-button type="primary" size="large" :loading="submitting" @click="completeRepair">完成维修</el-button></div>
</main>
<aside class="side-column"><section class="panel operator-panel"><h2>当前操作人</h2><div><span>姓名</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>当前任务</span><strong>{{ task?.device_no || '未选择' }}</strong></div></section><section class="panel tip-panel"><h2>处理提示</h2><p>物料不良请登记替换备件;作业不良需选择责任人员;搭配错误需选择对应物料分类。</p><el-button link type="primary" @click="goTasks">查看待维修订单</el-button></section></aside>
</div>
</div>
<el-dialog v-model="partDialogVisible" title="添加维修备件" width="460px"><el-input v-model="partCode" autofocus placeholder="请输入商品编码" @keyup.enter="addPart" /><template #footer><el-button @click="partDialogVisible = false">取消</el-button><el-button type="primary" :loading="loadingPart" @click="addPart">确定</el-button></template></el-dialog>
</div>
</template>
<script setup lang="ts">
import { CircleCheckFilled, Grid, EditPen, Tools, Box } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import type { UploadUserFile } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { navigateTo, useRoute } from 'nuxt/app'
import { useMemberStore } from '~/stores/member'
import { getFaultDescriptionOptions, getMaterialCategories, getRepairStatistics, getRepairTask, getScanRecords, lookupMaterial, submitRepairRecord, uploadProduceImage, type ProduceRepairTask } from '~/apps/produce/api/exception'
interface Part { id: string; product_code: string; product_name?: string; category_id?: string | number; category_name?: string; source: string; quantity: number; fault_description_ids: string[] }
interface ScanRecord { id: string | number; process_name?: string; step_name?: string; operator?: string; user_code?: string }
interface FaultDescription { id: string | number; category_id: string | number; description: string }
const memberStore = useMemberStore()
const currentUser = computed(() => ({ name: String(memberStore.info?.nickname || memberStore.info?.real_name || memberStore.info?.username || ''), code: String(memberStore.info?.username || memberStore.info?.user_code || '') }))
const summaries = ref([{ label: '待维修', value: 0, type: 'warning', action: true }, { label: '处理中', value: 0, type: 'primary' }, { label: '今日完成', value: 0, type: 'success' }])
const resultOptions = [{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: Box }, { label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: EditPen }, { label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: Grid }]
const deviceNo = ref('')
const task = ref<ProduceRepairTask | null>(null)
const categories = ref<Array<{ id: string | number; category_name: string }>>([])
const scanRecords = ref<ScanRecord[]>([])
const faults = ref<FaultDescription[]>([])
const imageFiles = ref<UploadUserFile[]>([])
const form = ref({ repair_result: 'material_defect', category_ids: [] as string[], responsible_ids: [] as string[], parts: [] as Part[], remark: '' })
const loadingTask = ref(false)
const loadingPart = ref(false)
const submitting = ref(false)
const partDialogVisible = ref(false)
const partCode = ref('')
const reasonText = computed(() => [task.value?.reason_name, task.value?.issue_name].filter(Boolean).join(' · ') || '-')
const loadStatistics = async () => {
const data = await getRepairStatistics({ user_code: currentUser.value.code }).catch(() => null)
if (!data) return
summaries.value = [
{ label: '待维修', value: Number(data.pending || 0), type: 'warning', action: true },
{ label: '处理中', value: Number(data.processing || 0), type: 'primary' },
{ label: '今日完成', value: Number(data.completed_today || 0), type: 'success' },
]
}
const loadTask = async () => { const value = deviceNo.value.trim(); if (!value || loadingTask.value) return ElMessage.warning('请输入设备编码或订单号'); loadingTask.value = true; try { task.value = await getRepairTask(value); form.value.responsible_ids = []; scanRecords.value = []; if (task.value?.device_no) { const result = await getScanRecords({ order_no: task.value.device_no, limit: 50 }); scanRecords.value = Array.isArray(result?.items) ? result.items : []; } } finally { loadingTask.value = false } }
const selectResult = (value: string) => { form.value.repair_result = value; if (value !== 'assembly_mismatch') form.value.category_ids = []; if (value !== 'operation_defect') form.value.responsible_ids = [] }
const openPartDialog = () => { partCode.value = ''; partDialogVisible.value = true }
const addPart = async () => { const code = partCode.value.trim(); if (!code) return ElMessage.warning('请输入商品编码'); if (form.value.parts.some((item: Part) => item.product_code === code)) return ElMessage.warning('该物料已添加'); loadingPart.value = true; try { const material = await lookupMaterial(code); const part: Part = { id: `${Date.now()}-${code}`, product_code: material.product_code || code, product_name: material.product_name, category_id: material.category_id, category_name: material.category_name, source: 'new', quantity: 1, fault_description_ids: [] }; form.value.parts.push(part); await loadFaults(); partDialogVisible.value = false } finally { loadingPart.value = false } }
const loadFaults = async () => { const ids = [...new Set(form.value.parts.map((item: Part) => String(item.category_id || '')).filter(Boolean))]; faults.value = ids.length ? await getFaultDescriptionOptions(ids) : [] }
const faultsForPart = (part: Part) => faults.value.filter((item: FaultDescription) => String(item.category_id) === String(part.category_id))
const removePart = (index: number) => { form.value.parts.splice(index, 1); void loadFaults() }
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
const resetForm = () => { deviceNo.value = ''; task.value = null; scanRecords.value = []; form.value = { repair_result: 'material_defect', category_ids: [], responsible_ids: [], parts: [], remark: '' }; imageFiles.value = [] }
const completeRepair = async () => { if (!task.value?.id) return ElMessage.warning('请先查询有效的维修任务'); if (!currentUser.value.code) return ElMessage.warning('请先登录'); if (form.value.repair_result === 'assembly_mismatch' && !form.value.category_ids.length) return ElMessage.warning('请选择搭配错误分类'); if (form.value.repair_result === 'operation_defect' && !form.value.responsible_ids.length) return ElMessage.warning('请选择责任人员'); submitting.value = true; try { const images: string[] = []; for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'repair'); if (url) images.push(url) } const response = await submitRepairRecord({ exception_id: task.value.id, user_code: currentUser.value.code, repair_result: form.value.repair_result, images, category_ids: form.value.category_ids, parts: form.value.parts.map((part: Part) => ({ product_code: part.product_code, source: part.source, quantity: part.quantity, fault_description_ids: part.fault_description_ids })), operation_fault_steps: form.value.responsible_ids, responsible_operators: form.value.responsible_ids, remark: form.value.remark.trim() }); if (response?.code !== 0) { ElMessage.error(response?.msg || '维修提交失败'); return } ElMessage.success('维修记录已完成'); resetForm(); void loadStatistics() } finally { submitting.value = false } }
const goTasks = () => navigateTo('/produce/repair/tasks')
onMounted(() => { void loadStatistics(); void getMaterialCategories().then((data: Array<{ id: string | number; category_name: string }>) => { categories.value = Array.isArray(data) ? data : [] }); const query = useRoute().query.device_no; if (query) { deviceNo.value = String(query); void loadTask() } })
</script>
<style scoped>
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;padding:32px 36px;border-radius:22px;color:#fff;box-shadow:0 14px 32px rgba(37,99,235,.12)}.repair-hero{background:linear-gradient(135deg,#2f6cf6,#35bfa3)}.eyebrow{font-size:13px;opacity:.85}h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.summary-card,.panel{background:#fff;border:1px solid #e7edf5;border-radius:18px;box-shadow:0 8px 24px rgba(36,59,92,.04)}.summary-card{display:flex;align-items:center;gap:12px;padding:18px 22px}.summary-card strong{font-size:28px}.summary-card span{color:#8a94a6;font-size:13px}.summary-card .el-button{margin-left:auto}.warning{color:#e59a2f}.primary{color:#2563eb}.success{color:#26b36c}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:20px;align-items:start}.main-column,.side-column{display:flex;flex-direction:column;gap:20px}.panel{padding:26px}.panel-title{display:flex;align-items:flex-start;gap:14px}.panel-title>span{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#eaf2ff;color:#2563eb;font-weight:700;flex:none}.panel-title h2{margin:0;font-size:17px}.panel-title p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.panel-title em{margin-left:auto;padding:5px 10px;border-radius:999px;color:#2563eb;background:#edf4ff;font-size:12px;font-style:normal}.panel-title em.optional{color:#8793a6;background:#f3f5f8}.panel-title small{margin-left:auto;color:#9aa4b4}.inline-form{display:flex;gap:12px;margin-top:20px}.inline-form .el-input{flex:1}.task-found{display:flex;align-items:center;gap:12px;margin-top:16px;padding:14px;border-radius:12px;background:#f0fbf6;color:#26b36c}.task-found>div{display:flex;flex-direction:column;gap:4px;flex:1}.task-found span{color:#2b3853;font-weight:600}.task-found .el-tag{margin-left:auto}.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.info-grid div{padding:14px;border-radius:12px;background:#f8fafc}.info-grid span,.info-grid strong{display:block}.info-grid span{color:#9aa4b4;font-size:12px}.info-grid strong{margin-top:8px;font-size:14px;word-break:break-all}.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:20px}.result-card{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:18px;border:1px solid #e6edf5;border-radius:14px;background:#f8fafc;color:#56647a;text-align:left;cursor:pointer}.result-card.active{border-color:#409eff;background:#edf4ff;color:#2563eb}.result-card small{color:#9aa4b4}.category-grid,.responsibility-grid{display:flex;flex-wrap:wrap;gap:14px;margin-top:22px}.responsibility-grid .el-checkbox{height:auto;margin:0;padding:12px 16px;border:1px solid #e6edf5;border-radius:12px;background:#f8fafc}.responsibility-grid .el-checkbox.is-checked{border-color:#409eff;background:#edf4ff}.responsibility-grid small{display:block;color:#9aa4b4}.part-toolbar{display:flex;justify-content:space-between;align-items:center;margin:22px 0 12px}.part-row{display:flex;align-items:flex-start;gap:12px;padding:16px 0;border-bottom:1px solid #eef2f7}.part-index{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#edf4ff;color:#2563eb}.part-info{display:flex;flex-direction:column;gap:8px;flex:1}.part-info>span{color:#8a94a6;font-size:12px}.fault-select{width:100%}.remark-input{margin-top:18px}.form-actions{display:flex;justify-content:flex-end;gap:12px}.operator-panel h2,.tip-panel h2{margin:0 0 18px}.operator-panel div{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #eef2f7;font-size:13px}.operator-panel div:last-child{border:0}.operator-panel span{color:#9aa4b4}.tip-panel p{color:#718096;font-size:13px;line-height:1.8}.upload-plus{font-size:30px;color:#409eff}:deep(.el-upload--picture-card),:deep(.el-upload-list__item){width:92px;height:92px}@media(max-width:900px){.produce-work-page{padding:16px}.content-grid{grid-template-columns:1fr}.side-column{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.page-hero{padding:24px;align-items:flex-start;flex-direction:column;gap:18px}.summary-grid,.side-column,.info-grid,.result-grid{grid-template-columns:1fr}.inline-form,.form-actions{flex-direction:column}.form-actions .el-button{width:100%}.part-row{flex-wrap:wrap}}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="produce-work-page tasks-page">
<div class="page-shell">
<section class="page-hero">
<div><span class="eyebrow">维修工作台</span><h1>待维修订单</h1><p>选择订单查看异常详情,并进入维修处理。</p></div>
<div class="task-count">{{ tasks.length }} 条</div>
</section>
<section class="panel">
<div class="panel-heading"><div><h2>待维修任务</h2><p>按异常提报时间展示尚未处理的维修任务</p></div><el-button :loading="loading" @click="loadTasks">刷新</el-button></div>
<el-skeleton v-if="loading" :rows="5" animated />
<el-empty v-else-if="!tasks.length" description="暂无待维修订单" :image-size="90" />
<div v-else class="task-grid">
<article v-for="task in tasks" :key="task.id" class="task-card">
<div class="task-card-header"><div><span class="task-label">设备 / 订单号</span><strong>{{ task.device_no }}</strong></div><el-tag type="warning">{{ task.status_name || '待维修' }}</el-tag></div>
<div class="task-detail"><span>异常类型</span><strong>{{ task.type_name || '-' }}</strong></div>
<div class="task-detail"><span>异常原因</span><strong>{{ reasonText(task) }}</strong></div>
<div class="task-footer"><span>{{ task.reported_date || '-' }}</span><span>{{ task.reporter || task.user_code || '-' }}</span></div>
<el-button type="primary" class="task-action" @click="startRepair(task.device_no)">开始维修</el-button>
</article>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getRepairTasks, type ProduceRepairTask } from '~/apps/produce/api/exception'
const loading = ref(false)
const tasks = ref<ProduceRepairTask[]>([])
const reasonText = (task: ProduceRepairTask) => [task.reason_name, task.issue_name].filter(Boolean).join(' · ') || '-'
const loadTasks = async () => {
loading.value = true
try {
const result = await getRepairTasks({ limit: 100 })
tasks.value = Array.isArray(result) ? result : []
} catch {
tasks.value = []
} finally {
loading.value = false
}
}
const startRepair = (deviceNo: string) => navigateTo({ path: '/produce/repair', query: { device_no: deviceNo } })
onMounted(() => { void loadTasks() })
</script>
<style scoped>
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:32px 36px;border-radius:22px;color:#fff;background:linear-gradient(135deg,#2f6cf6,#35bfa3);box-shadow:0 14px 32px rgba(37,99,235,.12)}.eyebrow{font-size:13px;opacity:.85}.page-hero h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.task-count{padding:10px 16px;border-radius:999px;background:rgba(255,255,255,.16);font-size:14px}.panel{margin-top:20px;padding:26px;border:1px solid #e7edf5;border-radius:18px;background:#fff;box-shadow:0 8px 24px rgba(36,59,92,.04)}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:22px}.panel-heading h2{margin:0;font-size:18px}.panel-heading p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.task-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.task-card{padding:20px;border:1px solid #e7edf5;border-radius:16px;background:#fbfcfe}.task-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-label,.task-detail span{display:block;color:#9aa4b4;font-size:12px}.task-card-header strong{display:block;margin-top:7px;color:#2563eb;font-size:16px;word-break:break-all}.task-detail{display:flex;justify-content:space-between;gap:16px;padding:14px 0;border-bottom:1px solid #eef2f7}.task-detail strong{max-width:68%;text-align:right;font-size:13px;word-break:break-all}.task-footer{display:flex;justify-content:space-between;gap:12px;padding:14px 0;color:#8a94a6;font-size:12px}.task-action{width:100%;margin-top:4px}@media(max-width:1000px){.task-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.produce-work-page{padding:16px}.page-hero{padding:24px;align-items:flex-start;flex-direction:column}.task-grid{grid-template-columns:1fr}.panel{padding:18px}}
</style>
@@ -0,0 +1,42 @@
export default [
{
path: '/produce/data-entry',
component: () => import('~/apps/produce/pages/data-entry/index.vue'),
meta: {
title: '数据录入',
layout: 'default',
},
},
{
path: '/produce/exception',
component: () => import('~/apps/produce/pages/exception/index.vue'),
meta: {
title: '异常处理',
layout: 'default',
},
},
{
path: '/produce/repair',
component: () => import('~/apps/produce/pages/repair/index.vue'),
meta: {
title: '维修处理',
layout: 'default',
},
},
{
path: '/produce/repair/tasks',
component: () => import('~/apps/produce/pages/repair/tasks.vue'),
meta: {
title: '待维修订单',
layout: 'default',
},
},
{
path: '/produce/query',
component: () => import('~/apps/produce/pages/query/index.vue'),
meta: {
title: '综合查询',
layout: 'default',
},
},
]