feat: add article and production modules
This commit is contained in:
@@ -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>
|
||||
+175
@@ -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>
|
||||
+207
@@ -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>
|
||||
+324
@@ -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>
|
||||
+237
@@ -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>
|
||||
+228
@@ -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>
|
||||
+127
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
+22
@@ -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>
|
||||
+100
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
+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>
|
||||
+42
@@ -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>
|
||||
+50
@@ -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>
|
||||
+185
@@ -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>
|
||||
+154
@@ -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>
|
||||
+249
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
+49
@@ -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>
|
||||
+135
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
+22
@@ -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>
|
||||
+490
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
+22
@@ -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>
|
||||
+100
@@ -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>
|
||||
+254
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
+310
@@ -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>
|
||||
+290
@@ -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>
|
||||
+26
@@ -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>
|
||||
+290
@@ -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>
|
||||
+218
@@ -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>
|
||||
+227
@@ -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>
|
||||
+277
@@ -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 })
|
||||
}
|
||||
+549
@@ -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>
|
||||
+610
@@ -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>
|
||||
+340
@@ -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',
|
||||
},
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user