feat: add article and production modules

This commit is contained in:
2026-08-31 13:16:46 +08:00
parent ac3da246b2
commit 5c586a1a95
334 changed files with 34419 additions and 1 deletions
-1
View File
@@ -8,7 +8,6 @@ dist-ssr
# 忽略 src/apps 目录下除了 demo 之外的内容
src/apps/*
!src/apps/demo/
@@ -0,0 +1,88 @@
import BaseService from '@/api/core/base'
import request from '@/utils/http'
export interface ArticleCategoryRow {
id?: string | number
name: string
slug?: string
parent_id?: string | number
sort?: number
enabled?: number
description?: string
created_date?: string
updated_date?: string
}
export interface ArticleTagRow {
id?: string | number
name: string
slug?: string
sort?: number
heat?: number
enabled?: number
description?: string
created_date?: string
updated_date?: string
}
export interface ArticleContentRow {
id?: string | number
title: string
slug?: string
category_id?: string | number
category_name?: string
cover?: string
summary?: string
content?: string
status?: number
is_top?: number
sort?: number
view_count?: number
published_at?: number | string
tag_ids?: Array<string | number>
tag_names?: string
created_date?: string
updated_date?: string
}
export const ArticleContentService = {
...BaseService<ArticleContentRow>({ baseUrl: '/article/index' })
}
export const ArticleCategoryService = {
...BaseService<ArticleCategoryRow>({ baseUrl: '/article/category' })
}
export const ArticleTagService = {
...BaseService<ArticleTagRow>({ baseUrl: '/article/tag' })
}
const pluginApi = {
async test(params = {}) {
return request.get<{ msg?: string; data?: unknown }>({
url: '/article/index',
params
})
},
contentList(params = {}) {
return ArticleContentService.list(params as Record<string, any>)
},
categoryList(params = {}) {
return ArticleCategoryService.list(params as Record<string, any>)
},
tagList(params = {}) {
return ArticleTagService.list(params as Record<string, any>)
},
async recycleList(params = {}) {
return request.get<{ code: number; msg: string; data: Record<string, unknown>[] }>({
url: '/article/recycle',
params
})
}
}
export default pluginApi
@@ -0,0 +1,60 @@
{
"article": {
"title": "Article Management",
"description": "Article content operation and management plugin",
"card_title": "Article Plugin Data",
"refresh_btn": "Refresh",
"common": {
"list_mode": "List Mode",
"columns": {
"index": "No."
}
},
"modules": {
"content": {
"title": "Content Center",
"description": "Manage articles, publish states, and content archives.",
"columns": {
"title": "Article Title",
"category": "Category",
"tags": "Tags",
"status": "Status",
"updated_at": "Last Updated"
}
},
"category": {
"title": "Content Category",
"description": "Manage article category structure and ordering.",
"columns": {
"name": "Category Name",
"slug": "Category Slug",
"article_count": "Article Count",
"sort": "Sort",
"updated_at": "Last Updated"
}
},
"tag": {
"title": "Tag Management",
"description": "Maintain article tags, popularity, and related article counts.",
"columns": {
"name": "Tag Name",
"slug": "Tag Slug",
"article_count": "Article Count",
"heat": "Heat",
"updated_at": "Last Updated"
}
},
"recycle": {
"title": "Content Recycle Bin",
"description": "Manage deleted articles for later recovery or cleanup.",
"columns": {
"title": "Article Title",
"category": "Original Category",
"deleted_by": "Deleted By",
"deleted_at": "Deleted At",
"expire_at": "Expire At"
}
}
}
}
}
@@ -0,0 +1,8 @@
{
"article": {
"title": "Article Management",
"description": "Article plugin demo page",
"card_title": "Article Plugin Data",
"refresh_btn": "Refresh"
}
}
@@ -0,0 +1,8 @@
{
"article": {
"title": "文章管理",
"description": "文章插件示例页面",
"card_title": "文章插件数据",
"refresh_btn": "刷新"
}
}
@@ -0,0 +1,108 @@
{
"article": {
"title": "文章管理",
"description": "文章内容运营与管理插件",
"card_title": "文章插件数据",
"refresh_btn": "刷新",
"common": {
"list_mode": "列表模式",
"columns": {
"index": "序号"
}
},
"modules": {
"content": {
"title": "内容中心",
"description": "管理文章内容、发布状态与内容归档。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入文章标题、分类或标签"
},
"form": {
"title_placeholder": "请输入文章标题",
"slug_placeholder": "请输入文章标识",
"category_id": "内容分类ID",
"cover": "封面图",
"cover_placeholder": "请输入封面图地址",
"summary": "摘要",
"summary_placeholder": "请输入摘要",
"content": "文章内容",
"content_placeholder": "请输入文章内容",
"status": "状态",
"is_top": "是否置顶",
"sort_placeholder": "请输入排序",
"published_at": "发布时间",
"tag_ids": "标签ID"
},
"columns": {
"title": "文章标题",
"category": "内容分类",
"tags": "标签",
"status": "状态",
"is_top": "置顶",
"updated_at": "最后更新"
}
},
"category": {
"title": "内容分类",
"description": "管理文章分类结构与分类排序。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入分类名称或分类标识"
},
"form": {
"name_placeholder": "请输入分类名称",
"slug_placeholder": "请输入分类标识",
"parent_id": "父级分类ID",
"description": "分类描述",
"description_placeholder": "请输入分类描述"
},
"columns": {
"name": "分类名称",
"slug": "分类标识",
"article_count": "内容数量",
"enabled": "状态",
"sort": "排序",
"updated_at": "最后更新"
}
},
"tag": {
"title": "标签管理",
"description": "维护文章标签、热度与关联内容数量。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入标签名称或标签标识"
},
"form": {
"name_placeholder": "请输入标签名称",
"slug_placeholder": "请输入标签标识",
"description": "标签描述",
"description_placeholder": "请输入标签描述"
},
"columns": {
"name": "标签名称",
"slug": "标签标识",
"article_count": "内容数量",
"enabled": "状态",
"heat": "热度",
"updated_at": "最后更新"
}
},
"recycle": {
"title": "内容回收站",
"description": "管理已删除文章,支持后续恢复或清理。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入文章标题或删除人"
},
"columns": {
"title": "文章标题",
"category": "原分类",
"deleted_by": "删除人",
"deleted_at": "删除时间",
"expire_at": "过期清理时间"
}
}
}
}
}
@@ -0,0 +1,59 @@
import { AppRouteRecord } from '@/types/router'
import { RoutesAlias } from '@/router/routesAlias'
export default {
path: '/article',
name: 'Article',
component: RoutesAlias.Layout,
meta: {
title: '文章管理',
icon: 'ri:plugin-line',
module: 'article'
},
children: [
{
path: 'index',
name: 'ArticleIndex',
component: '/article/content/index',
meta: {
title: '内容中心',
icon: 'ri:article-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'category',
name: 'ArticleCategory',
component: '/article/category/index',
meta: {
title: '内容分类',
icon: 'ri:folder-2-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'tag',
name: 'ArticleTag',
component: '/article/tag/index',
meta: {
title: '标签管理',
icon: 'ri:price-tag-3-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'recycle',
name: 'ArticleRecycle',
component: '/article/recycle/index',
meta: {
title: '内容回收站',
icon: 'ri:delete-bin-6-line',
keepAlive: false,
module: 'article'
}
}
]
} as AppRouteRecord
@@ -0,0 +1,26 @@
<template>
<div class="article-category-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleCategoryList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-category-page {
height: 100%;
}
</style>
@@ -0,0 +1,164 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleCategoryService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ArticleCategoryService.list,
dialogTitle: $t('article.modules.category.title'),
crudApi: {
add: ArticleCategoryService.create,
edit: ArticleCategoryService.update,
remove: ArticleCategoryService.delete,
view: ArticleCategoryService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:category:create',
editAuth: 'article:category:update',
removeAuth: 'article:category:delete',
viewAuth: 'article:category:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_name',
label: $t('article.modules.category.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.category.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'selection',
},
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'name',
label: $t('article.modules.category.columns.name'),
minWidth: 220,
},
{
prop: 'slug',
label: $t('article.modules.category.columns.slug'),
minWidth: 180,
},
{
prop: 'enabled',
label: $t('article.modules.category.columns.enabled'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
},
},
{
prop: 'sort',
label: $t('article.modules.category.columns.sort'),
width: 120,
},
{
prop: 'updated_date',
label: $t('article.modules.category.columns.updated_at'),
minWidth: 180,
},
],
dialogProps: {
width: '600px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'name',
label: $t('article.modules.category.columns.name'),
componentProps: {
placeholder: $t('article.modules.category.form.name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: $t('article.modules.category.columns.slug'),
componentProps: {
placeholder: $t('article.modules.category.form.slug_placeholder'),
maxlength: 100,
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'parent_id',
label: $t('article.modules.category.form.parent_id'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('article.modules.category.columns.sort'),
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'enabled',
label: $t('article.modules.category.columns.enabled'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('article.modules.category.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('article.modules.category.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="article-content-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleContentList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-content-page {
height: 100%;
}
</style>
@@ -0,0 +1,212 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleContentService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ArticleContentService.list,
dialogTitle: $t('article.modules.content.title'),
crudApi: {
add: ArticleContentService.create,
edit: ArticleContentService.update,
remove: ArticleContentService.delete,
view: ArticleContentService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:create',
editAuth: 'article:update',
removeAuth: 'article:delete',
viewAuth: 'article:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_title',
label: $t('article.modules.content.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.content.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'selection',
},
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'title',
label: $t('article.modules.content.columns.title'),
minWidth: 220,
},
{
prop: 'category_name',
label: $t('article.modules.content.columns.category'),
minWidth: 160,
},
{
prop: 'tag_names',
label: $t('article.modules.content.columns.tags'),
minWidth: 200,
},
{
prop: 'status',
label: $t('article.modules.content.columns.status'),
width: 120,
},
{
prop: 'is_top',
label: $t('article.modules.content.columns.is_top'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
},
},
{
prop: 'updated_date',
label: $t('article.modules.content.columns.updated_at'),
minWidth: 180,
},
],
dialogProps: {
width: '70%',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'title',
label: $t('article.modules.content.columns.title'),
componentProps: {
placeholder: $t('article.modules.content.form.title_placeholder'),
maxlength: 200,
},
rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: 'Slug',
componentProps: {
placeholder: $t('article.modules.content.form.slug_placeholder'),
maxlength: 200,
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'category_id',
label: $t('article.modules.content.form.category_id'),
componentProps: {
min: 0,
class: 'w-full',
},
rules: [{ required: true, message: $t('article.modules.content.form.category_id') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'cover',
label: $t('article.modules.content.form.cover'),
componentProps: {
placeholder: $t('article.modules.content.form.cover_placeholder'),
},
colSpan: 12,
},
{
component: 'Input',
prop: 'summary',
label: $t('article.modules.content.form.summary'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('article.modules.content.form.summary_placeholder'),
},
colSpan: 24,
},
{
component: 'Input',
prop: 'content',
label: $t('article.modules.content.form.content'),
componentProps: {
type: 'textarea',
rows: 10,
placeholder: $t('article.modules.content.form.content_placeholder'),
},
colSpan: 24,
},
{
component: 'InputNumber',
prop: 'status',
label: $t('article.modules.content.form.status'),
defaultValue: 1,
componentProps: {
min: 0,
max: 2,
class: 'w-full',
},
colSpan: 8,
},
{
component: 'ApiDict',
prop: 'is_top',
label: $t('article.modules.content.form.is_top'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 8,
},
{
component: 'InputNumber',
prop: 'sort',
label: 'Sort',
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 8,
},
{
component: 'DatePicker',
prop: 'published_at',
label: $t('article.modules.content.form.published_at'),
componentProps: {
type: 'datetime',
valueFormat: 'X',
class: 'w-full',
},
colSpan: 12,
},
],
},
}
}
@@ -0,0 +1,147 @@
<template>
<div class="article-container">
<div class="plugin-header">
<h1>{{ $t('article.title') }}</h1>
<p>{{ $t('article.description') }}</p>
</div>
<div class="plugin-content">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>{{ $t('article.card_title') }}</span>
<el-button type="primary" @click="fetchTestData">
{{ $t('article.refresh_btn') }}
</el-button>
</div>
</template>
<div v-loading="loading" class="card-body">
<div v-if="error" class="error-message">
<el-alert
:title="error"
type="error"
show-icon
/>
</div>
<div v-else-if="testData" class="plugin-data">
<el-descriptions :column="1" border>
<el-descriptions-item label="测试消息">
{{ testData.msg }}
</el-descriptions-item>
<el-descriptions-item label="数据">
{{ testData.data }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else class="empty-state">
<el-empty description="暂无数据" />
</div>
</div>
</el-card>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import pluginApi from '../../../api/index';
export default defineComponent({
name: 'ArticleView',
setup() {
const loading = ref(false);
const error = ref<string | null>(null);
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
const fetchTestData = async () => {
loading.value = true;
error.value = null;
try {
const response = await pluginApi.test();
testData.value = response;
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : '网络错误';
} finally {
loading.value = false;
}
};
// 初始加载数据
fetchTestData();
return {
loading,
error,
testData,
fetchTestData
}
}
});
</script>
<style scoped>
.article-container {
padding: 20px;
min-height: 80vh;
}
.plugin-header {
margin-bottom: 30px;
text-align: center;
}
.plugin-header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
}
.plugin-header p {
font-size: 16px;
color: #606266;
}
.plugin-content {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.error-message {
margin-bottom: 20px;
}
.plugin-data {
margin-top: 20px;
}
.plugin-data pre {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,26 @@
<template>
<div class="article-recycle-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleRecycleList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-recycle-page {
height: 100%;
}
</style>
@@ -0,0 +1,65 @@
import { CrudSchema } from '@/components/crud'
import { $t } from '@/locales'
import pluginApi from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: async (params: any) => {
const response = await pluginApi.recycleList(params)
const rows = Array.isArray(response?.data) ? response.data : []
return {
recordCount: rows.length,
totalPage: 1,
rows,
}
},
useCrud: false,
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_title',
label: $t('article.modules.recycle.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.recycle.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'title',
label: $t('article.modules.recycle.columns.title'),
minWidth: 220,
},
{
prop: 'category',
label: $t('article.modules.recycle.columns.category'),
minWidth: 160,
},
{
prop: 'deleted_by',
label: $t('article.modules.recycle.columns.deleted_by'),
width: 140,
},
{
prop: 'deleted_at',
label: $t('article.modules.recycle.columns.deleted_at'),
minWidth: 180,
},
{
prop: 'expire_at',
label: $t('article.modules.recycle.columns.expire_at'),
minWidth: 180,
},
],
}
}
@@ -0,0 +1,26 @@
<template>
<div class="article-tag-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleTagList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-tag-page {
height: 100%;
}
</style>
@@ -0,0 +1,164 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleTagService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ArticleTagService.list,
dialogTitle: $t('article.modules.tag.title'),
crudApi: {
add: ArticleTagService.create,
edit: ArticleTagService.update,
remove: ArticleTagService.delete,
view: ArticleTagService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:tag:create',
editAuth: 'article:tag:update',
removeAuth: 'article:tag:delete',
viewAuth: 'article:tag:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_name',
label: $t('article.modules.tag.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.tag.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'selection',
},
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'name',
label: $t('article.modules.tag.columns.name'),
minWidth: 220,
},
{
prop: 'slug',
label: $t('article.modules.tag.columns.slug'),
minWidth: 180,
},
{
prop: 'enabled',
label: $t('article.modules.tag.columns.enabled'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
},
},
{
prop: 'heat',
label: $t('article.modules.tag.columns.heat'),
width: 120,
},
{
prop: 'updated_date',
label: $t('article.modules.tag.columns.updated_at'),
minWidth: 180,
},
],
dialogProps: {
width: '600px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'name',
label: $t('article.modules.tag.columns.name'),
componentProps: {
placeholder: $t('article.modules.tag.form.name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: $t('article.modules.tag.columns.slug'),
componentProps: {
placeholder: $t('article.modules.tag.form.slug_placeholder'),
maxlength: 100,
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('article.modules.tag.columns.heat'),
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'heat',
label: $t('article.modules.tag.columns.heat'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'enabled',
label: $t('article.modules.tag.columns.enabled'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('article.modules.tag.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('article.modules.tag.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,147 @@
<template>
<div class="article-container">
<div class="plugin-header">
<h1>{{ $t('article.title') }}</h1>
<p>{{ $t('article.description') }}</p>
</div>
<div class="plugin-content">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>{{ $t('article.card_title') }}</span>
<el-button type="primary" @click="fetchTestData">
{{ $t('article.refresh_btn') }}
</el-button>
</div>
</template>
<div v-loading="loading" class="card-body">
<div v-if="error" class="error-message">
<el-alert
:title="error"
type="error"
show-icon
/>
</div>
<div v-else-if="testData" class="plugin-data">
<el-descriptions :column="1" border>
<el-descriptions-item label="测试消息">
{{ testData.msg }}
</el-descriptions-item>
<el-descriptions-item label="数据">
{{ testData.data }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else class="empty-state">
<el-empty description="暂无数据" />
</div>
</div>
</el-card>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import pluginApi from '../../api/index';
export default defineComponent({
name: 'ArticleView',
setup() {
const loading = ref(false);
const error = ref<string | null>(null);
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
const fetchTestData = async () => {
loading.value = true;
error.value = null;
try {
const response = await pluginApi.test();
testData.value = response;
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : '网络错误';
} finally {
loading.value = false;
}
};
// 初始加载数据
fetchTestData();
return {
loading,
error,
testData,
fetchTestData
}
}
});
</script>
<style scoped>
.article-container {
padding: 20px;
min-height: 80vh;
}
.plugin-header {
margin-bottom: 30px;
text-align: center;
}
.plugin-header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
}
.plugin-header p {
font-size: 16px;
color: #606266;
}
.plugin-content {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.error-message {
margin-bottom: 20px;
}
.plugin-data {
margin-top: 20px;
}
.plugin-data pre {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,118 @@
import BaseService from '@/api/core/base'
import request from '@/utils/http'
export interface ArticleCategoryRow {
id?: string | number
name: string
slug?: string
parent_id?: string | number
article_count?: number
sort?: number
enabled?: number
description?: string
created_date?: string
updated_date?: string
}
export interface ArticleTagRow {
id?: string | number
name: string
slug?: string
article_count?: number
sort?: number
heat?: number
enabled?: number
description?: string
created_date?: string
updated_date?: string
}
export interface ArticleContentRow {
id?: string | number
title: string
content_type?: string
slug?: string
category_id?: string | number
category_name?: string
channel?: string
visibility?: number
author?: string
source?: string
cover?: string
seo_keywords?: string
seo_title?: string
seo_description?: string
summary?: string
content?: string
status?: number
is_top?: number
is_recommend?: number
allow_comment?: number
sort?: number
view_count?: number
like_count?: number
comment_count?: number
favorite_count?: number
published_at?: number | string
offline_at?: number | string
tag_ids?: Array<string | number>
tag_names?: string
created_date?: string
updated_date?: string
}
export const ArticleContentService = {
...BaseService<ArticleContentRow>({ baseUrl: '/article/index' })
}
export const ArticleCategoryService = {
...BaseService<ArticleCategoryRow>({ baseUrl: '/article/category' })
}
export const ArticleTagService = {
...BaseService<ArticleTagRow>({ baseUrl: '/article/tag' })
}
const pluginApi = {
async test(params = {}) {
return request.get<{ msg?: string; data?: unknown }>({
url: '/article/index',
params
})
},
contentList(params = {}) {
return ArticleContentService.list(params as Record<string, any>)
},
categoryList(params = {}) {
return ArticleCategoryService.list(params as Record<string, any>)
},
tagList(params = {}) {
return ArticleTagService.list(params as Record<string, any>)
},
async recycleList(params = {}) {
return request.get<{ code: number; msg: string; data: Record<string, unknown>[] }>({
url: '/article/recycle',
params
})
},
async recycleRestore(id: string | number) {
return request.put({
url: `/article/recycle/${id}/restore`,
params: { id }
})
},
async recycleDelete(id: string | number) {
return request.del({
url: `/article/recycle/${id}`,
params: { id }
})
}
}
export default pluginApi
@@ -0,0 +1,136 @@
{
"article": {
"title": "Article Management",
"description": "Article content operation and management plugin",
"card_title": "Article Plugin Data",
"refresh_btn": "Refresh",
"common": {
"list_mode": "List Mode",
"columns": {
"index": "No."
}
},
"modules": {
"content": {
"title": "Content Center",
"description": "Manage articles, publish states, and content archives.",
"search": {
"keywords": "Keywords",
"keywords_placeholder": "Please enter article title, category, or tags"
},
"form": {
"title": "Content Title",
"title_placeholder": "Please enter article title",
"slug": "Content Slug",
"slug_placeholder": "Please enter article slug",
"content_type": "Content Type",
"category_id": "Category",
"category_id_placeholder": "Please select category",
"channel": "Channel",
"visibility": "Visibility",
"author": "Author",
"author_placeholder": "Please enter author",
"source": "Source",
"cover": "Cover",
"cover_placeholder": "Please enter cover URL",
"summary": "Summary",
"summary_placeholder": "Please enter summary",
"content": "Content",
"content_placeholder": "Please enter content",
"status": "Status",
"is_top": "Pinned",
"is_recommend": "Recommend",
"allow_comment": "Comment Setting",
"sort": "Sort Order",
"sort_placeholder": "Please enter sort order",
"view_count": "View Count",
"like_count": "Like Count",
"comment_count": "Comment Count",
"favorite_count": "Favorite Count",
"published_at": "Published At",
"offline_at": "Offline At",
"tag_ids": "Content Tags",
"tag_ids_placeholder": "Please select content tags",
"seo_keywords": "SEO Keywords",
"seo_keywords_placeholder": "For search engine optimization",
"seo_title": "SEO Title",
"seo_title_placeholder": "Optional, defaults to content title",
"seo_description": "SEO Description",
"seo_description_placeholder": "Optional, highlight value and search keywords"
},
"columns": {
"title": "Article Title",
"content_info": "Content Info",
"category": "Category",
"tags": "Tags",
"status": "Status",
"business_data": "Business Data",
"time_info": "Time",
"is_top": "Pinned",
"updated_at": "Last Updated"
}
},
"category": {
"title": "Content Category",
"description": "Manage article category structure and ordering.",
"search": {
"keywords": "Keywords",
"keywords_placeholder": "Please enter category name or slug"
},
"form": {
"name_placeholder": "Please enter category name",
"slug_placeholder": "Please enter category slug",
"parent_id": "Parent Category",
"parent_top": "Top Category",
"description": "Category Description",
"description_placeholder": "Please enter category description"
},
"columns": {
"name": "Category Name",
"slug": "Category Slug",
"article_count": "Content Count",
"enabled": "Status",
"sort": "Sort",
"updated_at": "Last Updated"
}
},
"tag": {
"title": "Tag Management",
"description": "Maintain article tags, popularity, and related article counts.",
"search": {
"keywords": "Keywords",
"keywords_placeholder": "Please enter tag name or slug"
},
"form": {
"name_placeholder": "Please enter tag name",
"slug_placeholder": "Please enter tag slug",
"description": "Tag Description",
"description_placeholder": "Please enter tag description"
},
"columns": {
"name": "Tag Name",
"slug": "Tag Slug",
"article_count": "Usage Count",
"enabled": "Status",
"heat": "Heat",
"updated_at": "Last Updated"
}
},
"recycle": {
"title": "Content Recycle Bin",
"description": "Manage deleted articles for later recovery or cleanup.",
"search": {
"keywords": "Keywords",
"keywords_placeholder": "Please enter article title or deleter"
},
"columns": {
"title": "Article Title",
"category": "Original Category",
"deleted_by": "Deleted By",
"deleted_at": "Deleted At",
"expire_at": "Expire At"
}
}
}
}
}
@@ -0,0 +1,8 @@
{
"article": {
"title": "Article Management",
"description": "Article plugin demo page",
"card_title": "Article Plugin Data",
"refresh_btn": "Refresh"
}
}
@@ -0,0 +1,8 @@
{
"article": {
"title": "文章管理",
"description": "文章插件示例页面",
"card_title": "文章插件数据",
"refresh_btn": "刷新"
}
}
@@ -0,0 +1,136 @@
{
"article": {
"title": "文章管理",
"description": "文章内容运营与管理插件",
"card_title": "文章插件数据",
"refresh_btn": "刷新",
"common": {
"list_mode": "列表模式",
"columns": {
"index": "序号"
}
},
"modules": {
"content": {
"title": "内容中心",
"description": "管理文章内容、发布状态与内容归档。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入文章标题、分类或标签"
},
"form": {
"title": "内容标题",
"title_placeholder": "请输入文章标题",
"slug": "内容别名",
"slug_placeholder": "请输入文章标识",
"content_type": "内容类型",
"category_id": "内容分类",
"category_id_placeholder": "请选择内容分类",
"channel": "所属频道",
"visibility": "可见范围",
"author": "作者",
"author_placeholder": "请输入作者",
"source": "内容来源",
"cover": "封面图",
"cover_placeholder": "请输入封面图地址",
"summary": "摘要",
"summary_placeholder": "请输入摘要",
"content": "文章内容",
"content_placeholder": "请输入文章内容",
"status": "状态",
"is_top": "是否置顶",
"is_recommend": "推荐",
"allow_comment": "评论设置",
"sort": "内容排序",
"sort_placeholder": "请输入排序",
"view_count": "浏览量",
"like_count": "点赞量",
"comment_count": "评论量",
"favorite_count": "收藏量",
"published_at": "发布时间",
"offline_at": "下线时间",
"tag_ids": "内容标签",
"tag_ids_placeholder": "请选择内容标签",
"seo_keywords": "SEO 关键词",
"seo_keywords_placeholder": "用于搜索引擎优化",
"seo_title": "SEO 标题",
"seo_title_placeholder": "可选,默认继承内容标题",
"seo_description": "SEO 描述",
"seo_description_placeholder": "可选,建议突出商业价值与检索关键词"
},
"columns": {
"title": "文章标题",
"content_info": "内容信息",
"category": "内容分类",
"tags": "标签",
"status": "状态",
"business_data": "经营数据",
"time_info": "时间",
"is_top": "置顶",
"updated_at": "最后更新"
}
},
"category": {
"title": "内容分类",
"description": "管理文章分类结构与分类排序。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入分类名称或分类标识"
},
"form": {
"name_placeholder": "请输入分类名称",
"slug_placeholder": "请输入分类标识",
"parent_id": "父级分类",
"parent_top": "顶级分类",
"description": "分类描述",
"description_placeholder": "请输入分类描述"
},
"columns": {
"name": "分类名称",
"slug": "分类标识",
"article_count": "内容数",
"enabled": "状态",
"sort": "排序",
"updated_at": "最后更新"
}
},
"tag": {
"title": "标签管理",
"description": "维护文章标签、热度与关联内容数量。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入标签名称或标签标识"
},
"form": {
"name_placeholder": "请输入标签名称",
"slug_placeholder": "请输入标签标识",
"description": "标签描述",
"description_placeholder": "请输入标签描述"
},
"columns": {
"name": "标签名称",
"slug": "标签标识",
"article_count": "使用次数",
"enabled": "状态",
"heat": "热度",
"updated_at": "最后更新"
}
},
"recycle": {
"title": "内容回收站",
"description": "管理已删除文章,支持后续恢复或清理。",
"search": {
"keywords": "关键字",
"keywords_placeholder": "请输入文章标题或删除人"
},
"columns": {
"title": "文章标题",
"category": "原分类",
"deleted_by": "删除人",
"deleted_at": "删除时间",
"expire_at": "过期清理时间"
}
}
}
}
}
@@ -0,0 +1,59 @@
import { AppRouteRecord } from '@/types/router'
import { RoutesAlias } from '@/router/routesAlias'
export default {
path: '/article',
name: 'Article',
component: RoutesAlias.Layout,
meta: {
title: '文章管理',
icon: 'ri:plugin-line',
module: 'article'
},
children: [
{
path: 'index',
name: 'ArticleIndex',
component: '/article/content/index',
meta: {
title: '内容中心',
icon: 'ri:article-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'category',
name: 'ArticleCategory',
component: '/article/category/index',
meta: {
title: '内容分类',
icon: 'ri:folder-2-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'tag',
name: 'ArticleTag',
component: '/article/tag/index',
meta: {
title: '标签管理',
icon: 'ri:price-tag-3-line',
keepAlive: false,
module: 'article'
}
},
{
path: 'recycle',
name: 'ArticleRecycle',
component: '/article/recycle/index',
meta: {
title: '内容回收站',
icon: 'ri:delete-bin-6-line',
keepAlive: false,
module: 'article'
}
}
]
} as AppRouteRecord
@@ -0,0 +1,26 @@
<template>
<div class="article-category-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleCategoryList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-category-page {
height: 100%;
}
</style>
@@ -0,0 +1,189 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleCategoryService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ArticleCategoryService.list,
dialogTitle: $t('article.modules.category.title'),
crudApi: {
add: ArticleCategoryService.create,
edit: ArticleCategoryService.update,
remove: ArticleCategoryService.delete,
view: ArticleCategoryService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:category:create',
editAuth: 'article:category:update',
removeAuth: 'article:category:delete',
viewAuth: 'article:category:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_name',
label: $t('article.modules.category.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.category.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'selection',
},
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'name',
label: $t('article.modules.category.columns.name'),
minWidth: 220,
},
{
prop: 'slug',
label: $t('article.modules.category.columns.slug'),
minWidth: 180,
},
{
prop: 'article_count',
label: $t('article.modules.category.columns.article_count'),
width: 120,
},
{
prop: 'enabled',
label: $t('article.modules.category.columns.enabled'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
},
},
{
prop: 'sort',
label: $t('article.modules.category.columns.sort'),
width: 120,
},
{
prop: 'updated_date',
label: $t('article.modules.category.columns.updated_at'),
minWidth: 180,
},
],
dialogProps: {
width: '600px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'name',
label: $t('article.modules.category.columns.name'),
componentProps: {
placeholder: $t('article.modules.category.form.name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: $t('article.modules.category.columns.slug'),
componentProps: {
placeholder: $t('article.modules.category.form.slug_placeholder'),
maxlength: 100,
},
colSpan: 12,
},
{
component: 'ApiTreeSelect',
prop: 'parent_id',
label: $t('article.modules.category.form.parent_id'),
defaultValue: 0,
componentProps: {
api: '/article/category',
requestMethod: 'get',
params: {
format: 'tree',
},
afterFetch: async (res: any) => {
if (!Array.isArray(res)) {
return res
}
return [
{
id: 0,
name: $t('article.modules.category.form.parent_top'),
},
...res,
]
},
clearable: true,
checkStrictly: true,
class: 'w-full',
labelField: 'name',
valueField: 'id',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('article.modules.category.columns.sort'),
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'enabled',
label: $t('article.modules.category.columns.enabled'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('article.modules.category.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('article.modules.category.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="article-content-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleContentList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-content-page {
height: 100%;
}
</style>
@@ -0,0 +1,544 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleContentService } from '../../../../api/index'
import { h } from 'vue'
const contentTypeOptions = [
{ label: '文章', value: 'article' },
{ label: '图文', value: 'news' },
{ label: '公告', value: 'notice' },
]
const channelOptions = [
{ label: '官网内容', value: 'official' },
{ label: '专题内容', value: 'topic' },
{ label: '活动内容', value: 'activity' },
]
const visibilityOptions = [
{ label: '公开', value: 1 },
{ label: '登录可见', value: 2 },
{ label: '私密', value: 3 },
]
const sourceOptions = [
{ label: '原创', value: 'original' },
{ label: '转载', value: 'repost' },
{ label: '投稿', value: 'contribution' },
]
const statusOptions = [
{ label: '草稿', value: 0 },
{ label: '发布', value: 1 },
{ label: '下线', value: 2 },
]
const getOptionLabel = (options: Array<{ label: string; value: string | number }>, value: string | number) => {
return options.find((item) => item.value === value)?.label || '-'
}
const getStatusTagClass = (status: number) => {
if (status === 1) {
return 'inline-flex rounded-full bg-[#ecfdf5] px-2 py-[2px] text-xs text-[#10b981]'
}
if (status === 2) {
return 'inline-flex rounded-full bg-[#fef2f2] px-2 py-[2px] text-xs text-[#ef4444]'
}
return 'inline-flex rounded-full bg-[#f4f4f5] px-2 py-[2px] text-xs text-[#909399]'
}
export const crudSchema = (): CrudSchema => {
return {
api: ArticleContentService.list,
dialogTitle: $t('article.modules.content.title'),
exportFileName: '内容中心',
exportFields: [
{ field: 'title', title: '标题' },
{ field: 'content_type', title: '内容类型' },
{ field: 'category_name', title: '分类' },
{ field: 'tag_names', title: '标签' },
{ field: 'author', title: '作者' },
{ field: 'channel', title: '频道' },
{ field: 'visibility', title: '可见范围' },
{ field: 'source', title: '来源' },
{ field: 'status', title: '状态' },
{ field: 'is_top', title: '置顶' },
{ field: 'is_recommend', title: '推荐' },
{ field: 'view_count', title: '浏览数' },
{ field: 'like_count', title: '点赞数' },
{ field: 'comment_count', title: '评论数' },
{ field: 'favorite_count', title: '收藏数' },
{ field: 'published_at', title: '发布时间' },
{ field: 'updated_date', title: '更新时间' },
{ field: 'summary', title: '摘要' }
],
crudApi: {
add: ArticleContentService.create,
edit: ArticleContentService.update,
remove: ArticleContentService.remove,
view: ArticleContentService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:create',
editAuth: 'article:update',
removeAuth: 'article:delete',
viewAuth: 'article:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_title',
label: $t('article.modules.content.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.content.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'content_info',
label: $t('article.modules.content.columns.content_info'),
minWidth: 380,
formatter: (row: any) => {
const metaItems = [
getOptionLabel(contentTypeOptions, row.content_type),
row.author || '管理员',
row.category_name,
row.tag_names,
].filter(Boolean)
const badgeNodes = [
Number(row.is_top) === 1
? h('span', { class: 'inline-flex items-center rounded bg-[#fff7e6] px-1.5 py-[1px] text-[10px] text-[#fa8c16]' }, '置顶')
: null,
Number(row.is_recommend) === 1
? h('span', { class: 'inline-flex items-center rounded bg-[#ecfdf5] px-1.5 py-[1px] text-[10px] text-[#10b981]' }, '推荐')
: null,
].filter(Boolean)
return h('div', { class: 'flex gap-3 py-2' }, [
row.cover
? h('img', {
src: row.cover,
class: 'mt-1 h-14 w-20 flex-none rounded bg-[#f5f7fa] object-cover border border-[#f0f0f0]'
})
: h(
'div',
{
class: 'mt-1 h-14 w-20 flex-none rounded border border-[#f0f0f0] bg-[#fafafa] text-[10px] text-[#c0c4cc] flex items-center justify-center'
},
'无上传封面'
),
h('div', { class: 'min-w-0 flex-1 leading-6' }, [
h('div', { class: 'flex items-center gap-2 text-sm font-medium text-[#1f2937]' }, [
h('span', { class: 'line-clamp-1' }, row.title || '-'),
...badgeNodes
]),
h(
'div',
{ class: 'mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-[#909399]' },
metaItems.length > 0 ? metaItems.map((item) => h('span', item)) : [h('span', '-')]
),
h('div', { class: 'mt-1 line-clamp-2 text-xs text-[#606266]' }, row.summary || row.slug || '-')
])
])
}
},
{
prop: 'status_info',
label: $t('article.modules.content.columns.status'),
width: 140,
align: 'center',
formatter: (row: any) => {
const statusText = getOptionLabel(statusOptions, row.status)
const visibilityText = getOptionLabel(visibilityOptions, row.visibility)
const sourceText = getOptionLabel(sourceOptions, row.source)
return h('div', { class: 'flex flex-col items-center py-2 text-sm leading-6' }, [
h('div', [
h('span', { class: getStatusTagClass(Number(row.status)) }, statusText)
]),
h('div', { class: 'mt-1 text-xs text-[#606266]' }, visibilityText),
h('div', { class: 'mt-1 text-xs text-[#909399]' }, sourceText)
])
}
},
{
prop: 'business_data',
label: $t('article.modules.content.columns.business_data'),
minWidth: 250,
formatter: (row: any) => {
const metricItems = [
{ label: '浏览', value: row.view_count ?? 0 },
{ label: '点赞', value: row.like_count ?? 0 },
{ label: '评论', value: row.comment_count ?? 0 },
{ label: '收藏', value: row.favorite_count ?? 0 },
]
return h('div', { class: 'grid grid-cols-2 gap-2 py-2 text-xs text-[#606266]' }, [
...metricItems.map((item) =>
h('div', { class: 'flex items-center justify-between rounded border border-[#e5e7eb] bg-[#eef2f6] px-3 py-1.5 transition-colors hover:bg-[#e5e7eb]' }, [
h('span', { class: 'text-[#909399]' }, item.label),
h('span', item.value)
])
)
])
}
},
{
prop: 'time_info',
label: $t('article.modules.content.columns.time_info'),
minWidth: 190,
formatter: (row: any) => {
return h('div', { class: 'py-2 text-xs leading-6 text-[#606266]' }, [
h('div', [
h('span', { class: 'text-[#909399]' }, '更新 '),
h('span', row.updated_date || '-')
]),
h('div', { class: 'mt-1' }, [
h('span', { class: 'text-[#909399]' }, '发布 '),
h('span', row.created_date || '-')
])
])
}
},
],
tableActionColumn: {
width: 170,
},
dialogProps: {
width: '70%',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'title',
label: $t('article.modules.content.columns.title'),
componentProps: {
placeholder: $t('article.modules.content.form.title_placeholder'),
maxlength: 200,
},
rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: $t('article.modules.content.form.slug'),
componentProps: {
placeholder: $t('article.modules.content.form.slug_placeholder'),
maxlength: 200,
},
colSpan: 12,
},
{
component: 'Select',
prop: 'content_type',
label: $t('article.modules.content.form.content_type'),
defaultValue: 'article',
componentProps: {
options: contentTypeOptions,
class: 'w-full',
},
rules: [{ required: true, message: $t('article.modules.content.form.content_type') }],
colSpan: 12,
},
{
component: 'ApiTreeSelect',
prop: 'category_id',
label: $t('article.modules.content.form.category_id'),
componentProps: {
api: '/article/category',
requestMethod: 'get',
params: {
format: 'tree',
},
teleported: true,
clearable: true,
checkStrictly: true,
class: 'w-full',
labelField: 'name',
valueField: 'id',
placeholder: $t('article.modules.content.form.category_id_placeholder'),
},
rules: [{ required: true, message: $t('article.modules.content.form.category_id') }],
colSpan: 12,
},
{
component: 'Select',
prop: 'channel',
label: $t('article.modules.content.form.channel'),
defaultValue: 'official',
componentProps: {
options: channelOptions,
class: 'w-full',
},
rules: [{ required: true, message: $t('article.modules.content.form.channel') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'author',
label: $t('article.modules.content.form.author'),
componentProps: {
placeholder: $t('article.modules.content.form.author_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('article.modules.content.form.author_placeholder') }],
colSpan: 12,
},
{
component: 'Select',
prop: 'visibility',
label: $t('article.modules.content.form.visibility'),
defaultValue: 1,
componentProps: {
options: visibilityOptions,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Select',
prop: 'status',
label: $t('article.modules.content.form.status'),
defaultValue: 0,
componentProps: {
options: statusOptions,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Select',
prop: 'source',
label: $t('article.modules.content.form.source'),
defaultValue: 'original',
componentProps: {
options: sourceOptions,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Avatar',
prop: 'cover',
label: $t('article.modules.content.form.cover'),
componentProps: {
shape: 'square',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'summary',
label: $t('article.modules.content.form.summary'),
componentProps: {
type: 'textarea',
rows: 4,
maxlength: 500,
placeholder: $t('article.modules.content.form.summary_placeholder'),
},
colSpan: 24,
},
{
component: 'ApiSelect',
prop: 'tag_ids',
label: $t('article.modules.content.form.tag_ids'),
componentProps: {
api: '/article/tag',
requestMethod: 'get',
params: {
format: 'select',
},
labelField: 'label',
valueField: 'value',
multiple: true,
clearable: true,
filterable: true,
class: 'w-full',
placeholder: $t('article.modules.content.form.tag_ids_placeholder'),
},
colSpan: 12,
},
{
component: 'Input',
prop: 'seo_keywords',
label: $t('article.modules.content.form.seo_keywords'),
componentProps: {
placeholder: $t('article.modules.content.form.seo_keywords_placeholder'),
maxlength: 255,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'seo_title',
label: $t('article.modules.content.form.seo_title'),
componentProps: {
placeholder: $t('article.modules.content.form.seo_title_placeholder'),
maxlength: 200,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'seo_description',
label: $t('article.modules.content.form.seo_description'),
componentProps: {
placeholder: $t('article.modules.content.form.seo_description_placeholder'),
maxlength: 500,
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('article.modules.content.form.sort'),
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 8,
},
{
component: 'DatePicker',
prop: 'published_at',
label: $t('article.modules.content.form.published_at'),
componentProps: {
type: 'datetime',
valueFormat: 'X',
class: 'w-full',
},
colSpan: 8,
},
{
component: 'DatePicker',
prop: 'offline_at',
label: $t('article.modules.content.form.offline_at'),
componentProps: {
type: 'datetime',
valueFormat: 'X',
class: 'w-full',
},
colSpan: 8,
},
{
component: 'InputNumber',
prop: 'view_count',
label: $t('article.modules.content.form.view_count'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 6,
},
{
component: 'InputNumber',
prop: 'like_count',
label: $t('article.modules.content.form.like_count'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 6,
},
{
component: 'InputNumber',
prop: 'comment_count',
label: $t('article.modules.content.form.comment_count'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 6,
},
{
component: 'InputNumber',
prop: 'favorite_count',
label: $t('article.modules.content.form.favorite_count'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 6,
},
{
component: 'ApiDict',
prop: 'is_top',
label: $t('article.modules.content.form.is_top'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 8,
},
{
component: 'ApiDict',
prop: 'is_recommend',
label: $t('article.modules.content.form.is_recommend'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 8,
},
{
component: 'ApiDict',
prop: 'allow_comment',
label: $t('article.modules.content.form.allow_comment'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 8,
},
{
component: 'Editor',
prop: 'content',
label: $t('article.modules.content.form.content'),
componentProps: {
placeholder: $t('article.modules.content.form.content_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,147 @@
<template>
<div class="article-container">
<div class="plugin-header">
<h1>{{ $t('article.title') }}</h1>
<p>{{ $t('article.description') }}</p>
</div>
<div class="plugin-content">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>{{ $t('article.card_title') }}</span>
<el-button type="primary" @click="fetchTestData">
{{ $t('article.refresh_btn') }}
</el-button>
</div>
</template>
<div v-loading="loading" class="card-body">
<div v-if="error" class="error-message">
<el-alert
:title="error"
type="error"
show-icon
/>
</div>
<div v-else-if="testData" class="plugin-data">
<el-descriptions :column="1" border>
<el-descriptions-item label="测试消息">
{{ testData.msg }}
</el-descriptions-item>
<el-descriptions-item label="数据">
{{ testData.data }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else class="empty-state">
<el-empty description="暂无数据" />
</div>
</div>
</el-card>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import pluginApi from '../../../api/index';
export default defineComponent({
name: 'ArticleView',
setup() {
const loading = ref(false);
const error = ref<string | null>(null);
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
const fetchTestData = async () => {
loading.value = true;
error.value = null;
try {
const response = await pluginApi.test();
testData.value = response;
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : '网络错误';
} finally {
loading.value = false;
}
};
// 初始加载数据
fetchTestData();
return {
loading,
error,
testData,
fetchTestData
}
}
});
</script>
<style scoped>
.article-container {
padding: 20px;
min-height: 80vh;
}
.plugin-header {
margin-bottom: 30px;
text-align: center;
}
.plugin-header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
}
.plugin-header p {
font-size: 16px;
color: #606266;
}
.plugin-content {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.error-message {
margin-bottom: 20px;
}
.plugin-data {
margin-top: 20px;
}
.plugin-data pre {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,59 @@
<template>
<div class="article-recycle-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { ElMessage, ElMessageBox } from 'element-plus'
import { $t } from '@/locales'
import pluginApi from '../../../api/index'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleRecycleList' })
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [
{
label: '恢复',
type: 'primary',
link: true,
icon: 'ant-design:swap-outlined',
auth: 'article:recycle:restore',
onClick: async (e: Event, row: any) => {
await pluginApi.recycleRestore(row.id)
ElMessage.success($t('common.operation_success'))
crudApi.reload()
}
},
{
label: '彻底删除',
type: 'danger',
link: true,
icon: 'ant-design:delete-outlined',
auth: 'article:recycle:delete',
onClick: async (e: Event, row: any) => {
await ElMessageBox.confirm('确定彻底删除该内容吗?删除后不可恢复。', '二次确认', {
type: 'warning',
confirmButtonText: '确定删除',
cancelButtonText: '取消'
})
await pluginApi.recycleDelete(row.id)
ElMessage.success($t('common.operation_success'))
crudApi.reload()
}
}
],
dropDownActions: [],
})
</script>
<style scoped>
.article-recycle-page {
height: 100%;
}
</style>
@@ -0,0 +1,65 @@
import { CrudSchema } from '@/components/crud'
import { $t } from '@/locales'
import pluginApi from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: async (params: any) => {
const response = await pluginApi.recycleList(params)
const rows = Array.isArray(response) ? response : []
return {
recordCount: rows.length,
totalPage: 1,
rows,
}
},
useCrud: false,
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_title',
label: $t('article.modules.recycle.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.recycle.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'title',
label: $t('article.modules.recycle.columns.title'),
minWidth: 220,
},
{
prop: 'category',
label: $t('article.modules.recycle.columns.category'),
minWidth: 160,
},
{
prop: 'deleted_by',
label: $t('article.modules.recycle.columns.deleted_by'),
width: 140,
},
{
prop: 'deleted_at',
label: $t('article.modules.recycle.columns.deleted_at'),
minWidth: 180,
},
{
prop: 'expire_at',
label: $t('article.modules.recycle.columns.expire_at'),
minWidth: 180,
},
],
}
}
@@ -0,0 +1,26 @@
<template>
<div class="article-tag-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ArticleTagList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.article-tag-page {
height: 100%;
}
</style>
@@ -0,0 +1,169 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ArticleTagService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ArticleTagService.list,
dialogTitle: $t('article.modules.tag.title'),
crudApi: {
add: ArticleTagService.create,
edit: ArticleTagService.update,
remove: ArticleTagService.delete,
view: ArticleTagService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'article:tag:create',
editAuth: 'article:tag:update',
removeAuth: 'article:tag:delete',
viewAuth: 'article:tag:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_name',
label: $t('article.modules.tag.search.keywords'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('article.modules.tag.search.keywords_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{
type: 'selection',
},
{
type: 'index',
label: $t('article.common.columns.index'),
width: 70,
},
{
prop: 'name',
label: $t('article.modules.tag.columns.name'),
minWidth: 220,
},
{
prop: 'slug',
label: $t('article.modules.tag.columns.slug'),
minWidth: 180,
},
{
prop: 'article_count',
label: $t('article.modules.tag.columns.article_count'),
width: 120,
},
{
prop: 'enabled',
label: $t('article.modules.tag.columns.enabled'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
},
},
{
prop: 'heat',
label: $t('article.modules.tag.columns.heat'),
width: 120,
},
{
prop: 'updated_date',
label: $t('article.modules.tag.columns.updated_at'),
minWidth: 180,
},
],
dialogProps: {
width: '600px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'name',
label: $t('article.modules.tag.columns.name'),
componentProps: {
placeholder: $t('article.modules.tag.form.name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'slug',
label: $t('article.modules.tag.columns.slug'),
componentProps: {
placeholder: $t('article.modules.tag.form.slug_placeholder'),
maxlength: 100,
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('article.modules.tag.columns.heat'),
defaultValue: 100,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'heat',
label: $t('article.modules.tag.columns.heat'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'enabled',
label: $t('article.modules.tag.columns.enabled'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_ENABLED_STATUS,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('article.modules.tag.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('article.modules.tag.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,147 @@
<template>
<div class="article-container">
<div class="plugin-header">
<h1>{{ $t('article.title') }}</h1>
<p>{{ $t('article.description') }}</p>
</div>
<div class="plugin-content">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>{{ $t('article.card_title') }}</span>
<el-button type="primary" @click="fetchTestData">
{{ $t('article.refresh_btn') }}
</el-button>
</div>
</template>
<div v-loading="loading" class="card-body">
<div v-if="error" class="error-message">
<el-alert
:title="error"
type="error"
show-icon
/>
</div>
<div v-else-if="testData" class="plugin-data">
<el-descriptions :column="1" border>
<el-descriptions-item label="测试消息">
{{ testData.msg }}
</el-descriptions-item>
<el-descriptions-item label="数据">
{{ testData.data }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else class="empty-state">
<el-empty description="暂无数据" />
</div>
</div>
</el-card>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import pluginApi from '../../api/index';
export default defineComponent({
name: 'ArticleView',
setup() {
const loading = ref(false);
const error = ref<string | null>(null);
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
const fetchTestData = async () => {
loading.value = true;
error.value = null;
try {
const response = await pluginApi.test();
testData.value = response;
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : '网络错误';
} finally {
loading.value = false;
}
};
// 初始加载数据
fetchTestData();
return {
loading,
error,
testData,
fetchTestData
}
}
});
</script>
<style scoped>
.article-container {
padding: 20px;
min-height: 80vh;
}
.plugin-header {
margin-bottom: 30px;
text-align: center;
}
.plugin-header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
}
.plugin-header p {
font-size: 16px;
color: #606266;
}
.plugin-content {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.error-message {
margin-bottom: 20px;
}
.plugin-data {
margin-top: 20px;
}
.plugin-data pre {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,519 @@
import request from "@/utils/http";
/**
* produce 插件 API
*/
import BaseService from "@/api/core/base";
export interface ProduceProcessRow {
id?: string | number;
process_code: string;
process_name: string;
description?: string;
step_count?: number;
sort?: number;
status?: number;
created_date?: string;
updated_date?: string;
}
export interface ProduceProcessStepRow {
id?: string | number;
process_id: string | number;
step_code: string;
step_name: string;
description?: string;
sort?: number;
status?: number;
prerequisite_steps?: Array<string | number> | string | null;
prerequisite_mode?: "all" | "any" | "none";
prerequisite_require_final?: number;
allow_repeat_scan?: number;
repeat_scan_statuses?: Array<string | number> | string | null;
process?: {
id?: string | number;
process_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStepStatusRow {
id?: string | number;
step_id: string | number;
status_code: string;
status_name: string;
is_initial?: number;
is_final?: number;
description?: string;
sort?: number;
status?: number;
step?: {
id?: string | number;
step_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStepRelationRow {
id?: string | number;
from_step_id: string | number;
to_step_id: string | number;
relation_type: "normal" | "exception";
flow_condition?: string | Record<string, any> | null;
priority?: number;
description?: string;
sort?: number;
status?: number;
from_step?: {
id?: string | number;
step_name?: string;
};
to_step?: {
id?: string | number;
step_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceStatusTransitionRow {
id?: string | number;
step_id: string | number;
from_status_id: string | number;
to_status_id: string | number;
trigger_event: string;
event_config?: string | Record<string, any> | null;
auto_trigger?: number;
priority?: number;
transition_type?: "primary" | "side_effect";
side_effect_type?: string;
side_effect_config?: string | Record<string, any> | null;
description?: string;
sort?: number;
status?: number;
step?: {
id?: string | number;
step_name?: string;
};
from_status?: {
id?: string | number;
status_name?: string;
};
to_status?: {
id?: string | number;
status_name?: string;
};
created_date?: string;
updated_date?: string;
}
export interface ProduceMaterialCategoryRow {
id?: string | number;
category_name: string;
created_date?: string;
updated_date?: string;
}
export interface ProduceMaterialRow {
id?: string | number;
category_id: string | number;
category_name?: string;
product_code: string;
product_name: string;
created_date?: string;
updated_date?: string;
}
export interface ProduceHardwareFaultDescriptionRow {
id?: string | number;
// ... other properties
}
export const ProduceMaterialCategoryService = {
...BaseService<ProduceMaterialCategoryRow>({
baseUrl: "/produce/material-category",
}),
};
export const ProduceMaterialService = {
...BaseService<ProduceMaterialRow>({ baseUrl: "/produce/material" }),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
importExcel(data: FormData) {
return request.post<{ created: number; updated: number }>({
url: "/produce/material/import",
data,
headers: { "Content-Type": "multipart/form-data" },
});
},
};
export const ProduceProcessService = {
...BaseService<ProduceProcessRow>({ baseUrl: "/produce/process" }),
};
export const ProduceProcessStepService = {
...BaseService<ProduceProcessStepRow>({ baseUrl: "/produce/process-step" }),
};
export const ProduceStepStatusService = {
...BaseService<ProduceStepStatusRow>({ baseUrl: "/produce/step-status" }),
};
export const ProduceStepRelationService = {
...BaseService<ProduceStepRelationRow>({ baseUrl: "/produce/step-relation" }),
};
export const ProduceStatusTransitionService = {
...BaseService<ProduceStatusTransitionRow>({
baseUrl: "/produce/status-transition",
}),
};
export interface ProduceTransitionLogRow {
id?: string | number;
work_order_id?: string | number | null;
work_order_no?: string;
step_id: string | number;
transition_id?: string | number | null;
from_status_id: string | number;
to_status_id: string | number;
trigger_event: string;
trigger_data?: Record<string, any> | string | null;
trigger_user?: string;
trigger_ip?: string;
is_auto?: number;
is_success?: number;
error_message?: string | null;
duration?: number;
remark?: string | null;
created_by?: string | number | null;
created_name?: string;
created_date?: string;
step?: {
id?: string | number;
step_name?: string;
};
transition?: {
id?: string | number;
trigger_event?: string;
};
from_status?: {
id?: string | number;
status_name?: string;
};
to_status?: {
id?: string | number;
status_name?: string;
};
}
export const ProduceTransitionLogService = {
...BaseService<ProduceTransitionLogRow>({
baseUrl: "/produce/transition-log",
allowedMethods: ["list", "get", "export"],
}),
};
export interface ProduceSpecificRuleRow {
id?: string | number;
rule_name: string;
rule_pattern: string;
rule_type: number;
apply_mode?: "order" | "machine" | "both";
match_field?: "order_no" | "process_order_no" | "product_code" | "user_code";
is_enabled?: number;
step_ids?: Array<string | number> | string | null;
sort?: number;
remark?: string;
created_name?: string;
created_date?: string;
}
export interface ProduceProductRecordRow {
id?: string | number;
process_order_no: string;
product_code: string;
quantity: number;
process_id?: string | number | null;
step_id?: string | number | null;
status_id?: string | number | null;
user_code?: string;
operator?: string;
operator_id?: string | number | null;
scan_result?: number;
remark?: string | null;
process?: {
id?: string | number;
process_name?: string;
};
step?: {
id?: string | number;
step_name?: string;
};
status?: {
id?: string | number;
status_name?: string;
};
created_name?: string;
created_date?: string;
}
export interface ProduceScanDataRow {
id?: string | number;
order_no: string;
quantity?: number;
user_code: string;
operator?: string;
operator_id?: string | number | null;
process_id: string | number;
step_id: string | number;
status_id?: string | number | null;
scan_result: number;
scan_time?: number;
scan_date?: string;
remark?: string | null;
process?: {
id?: string | number;
process_name?: string;
};
step?: {
id?: string | number;
step_name?: string;
};
status?: {
id?: string | number;
status_name?: string;
};
created_name?: string;
created_date?: string;
}
export const ProduceSpecificRuleService = {
...BaseService<ProduceSpecificRuleRow>({ baseUrl: "/produce/specific-rule" }),
};
export const ProduceProductRecordService = {
...BaseService<ProduceProductRecordRow>({
baseUrl: "/produce/product-record",
}),
};
export const ProduceScanDataService = {
...BaseService<ProduceScanDataRow>({ baseUrl: "/produce/scan-data" }),
};
export interface ProduceExceptionSettingRow {
id?: string | number;
parent_id?: string | number | null;
parent_name?: string;
setting_type: "type" | "reason" | "issue";
code: string;
name: string;
sort?: number;
is_enabled?: number;
remark?: string | null;
created_name?: string;
created_date?: string;
}
export interface ProduceExceptionRecordRow {
id?: string | number;
order_no: string;
user_code?: string | null;
reporter?: string | null;
type_id?: string | number | null;
type_code: string;
type_name: string;
reason_id?: string | number | null;
reason_code: string;
reason_name: string;
issue_id?: string | number | null;
issue_code?: string | null;
issue_name?: string | null;
images?: string[] | null;
remark?: string | null;
status?: number;
status_name?: string;
handler?: string | null;
handler_id?: string | number | null;
handled_at?: number | null;
handled_date?: string | null;
reported_at?: number | null;
reported_date?: string | null;
created_name?: string;
created_date?: string;
}
export const ProduceExceptionSettingService = {
...BaseService<ProduceExceptionSettingRow>({
baseUrl: "/produce/exception-setting",
}),
};
export interface ProduceRepairSettingRow {
id?: string | number;
parent_id?: string | number | null;
parent_name?: string;
setting_type: "type" | "reason" | "issue";
code: string;
name: string;
sort?: number;
is_enabled?: number;
remark?: string | null;
created_name?: string;
created_date?: string;
}
export interface ProduceHardwareFaultDescriptionRow {
id?: string | number;
category_id: string | number;
category_name?: string;
description: string;
use_count?: number;
is_enabled?: number;
remark?: string | null;
created_date?: string;
}
export interface ProduceRepairRecordRow {
id?: string | number;
repair_no: string;
exception_id?: string | number | null;
exception_reason?: string | null;
exception_issue?: string | null;
exception_type_name?: string | null;
order_no?: string | null;
product_code?: string | null;
product_name?: string | null;
category_id?: string | number | null;
category_name?: string | null;
fault_description_id?: string | number | null;
fault_description_ids?: Array<string | number>;
fault_description?: string | null;
fault_descriptions?: string[];
repair_result?: string | null;
parts?: Array<Record<string, any>> | string | null;
responsible_operators?: Array<Record<string, any>> | string | null;
operator?: string | null;
repaired_at?: number | null;
repaired_date?: string | null;
remark?: string | null;
created_date?: string;
}
export const ProduceRepairSettingService = {
...BaseService<ProduceRepairSettingRow>({
baseUrl: "/produce/repair-setting",
}),
};
export const ProduceHardwareFaultDescriptionService = {
...BaseService<ProduceHardwareFaultDescriptionRow>({
baseUrl: "/produce/hardware-fault-description",
}),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
importExcel(data: FormData) {
return request.post<{ created: number; updated: number }>({
url: "/produce/hardware-fault-description/import",
data,
headers: { "Content-Type": "multipart/form-data" },
});
},
};
export const ProduceRepairRecordService = {
...BaseService<ProduceRepairRecordRow>({ baseUrl: "/produce/repair-record" }),
categoryOptions(params: Record<string, any> = {}) {
return request.get({
url: "/produce/material-category",
params: { format: "select", ...params },
});
},
faultDescriptionOptions(params: Record<string, any> = {}) {
return ProduceHardwareFaultDescriptionService.list({
limit: 999,
is_enabled: 1,
...params,
});
},
};
export const ProduceExceptionRecordService = {
...BaseService<ProduceExceptionRecordRow>({
baseUrl: "/produce/exception-record",
}),
};
export const ProduceConfigApi = {
info(params: Record<string, any>) {
return request.get({ url: "/produce/config/info", params });
},
save(data: Record<string, any>) {
return request.post({ url: "/produce/config/save", data });
},
};
export default {
materialCategoryList(params = {}) {
return ProduceMaterialCategoryService.list(params as Record<string, any>);
},
materialList(params = {}) {
return ProduceMaterialService.list(params as Record<string, any>);
},
processList(params = {}) {
return ProduceProcessService.list(params as Record<string, any>);
},
processStepList(params = {}) {
return ProduceProcessStepService.list(params as Record<string, any>);
},
stepStatusList(params = {}) {
return ProduceStepStatusService.list(params as Record<string, any>);
},
stepRelationList(params = {}) {
return ProduceStepRelationService.list(params as Record<string, any>);
},
statusTransitionList(params = {}) {
return ProduceStatusTransitionService.list(params as Record<string, any>);
},
transitionLogList(params = {}) {
return ProduceTransitionLogService.list(params as Record<string, any>);
},
specificRuleList(params = {}) {
return ProduceSpecificRuleService.list(params as Record<string, any>);
},
productRecordList(params = {}) {
return ProduceProductRecordService.list(params as Record<string, any>);
},
scanDataList(params = {}) {
return ProduceScanDataService.list(params as Record<string, any>);
},
exceptionRecordList(params = {}) {
return ProduceExceptionRecordService.list(params as Record<string, any>);
},
exceptionSettingList(params = {}) {
return ProduceExceptionSettingService.list(params as Record<string, any>);
},
repairSettingList(params = {}) {
return ProduceRepairSettingService.list(params as Record<string, any>);
},
hardwareFaultDescriptionList(params = {}) {
return ProduceHardwareFaultDescriptionService.list(
params as Record<string, any>,
);
},
repairRecordList(params = {}) {
return ProduceRepairRecordService.list(params as Record<string, any>);
},
};
@@ -0,0 +1,50 @@
import request from '@/utils/http'
export interface WorkStatisticsQuery {
month?: string
date?: string
data_type?: string
process_id?: string | number
step_id?: string | number
scan_result?: string | number
user_code?: string
operator?: string
process_name?: string
step_name?: string
}
export interface WorkStatisticsColumn {
key: string
label: string
}
export interface WorkStatisticsOption {
label: string
value: string | number
}
export interface WorkStatisticsResponse {
columns: WorkStatisticsColumn[]
items: Array<Record<string, string | number | null>>
}
export const ProduceWorkStatisticsService = {
monthly(params: WorkStatisticsQuery = {}) {
return request.get<WorkStatisticsResponse>({ url: '/produce/statistics/monthly', params })
},
daily(params: WorkStatisticsQuery = {}) {
return request.get<WorkStatisticsResponse>({ url: '/produce/statistics/daily', params })
},
getProcessOptions() {
return request.get<WorkStatisticsOption[]>({
url: '/produce/process',
params: { format: 'select' }
})
},
getStepOptions(processId?: string | number) {
return request.get<WorkStatisticsOption[]>({
url: '/produce/process-step',
params: { format: 'select', process_id: processId || undefined }
})
}
}
@@ -0,0 +1,217 @@
{
"produce": {
"common": {
"columns": {
"index": "No."
}
},
"modules": {
"process": {
"title": "Process Management",
"search": {
"process_code": "Process Code",
"process_code_placeholder": "Please enter process code",
"process_name": "Process Name",
"process_name_placeholder": "Please enter process name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"process_code_placeholder": "Please enter process code",
"process_name_placeholder": "Please enter process name",
"description": "Process Description",
"description_placeholder": "Please enter process description"
},
"columns": {
"process_code": "Process Code",
"process_name": "Process Name",
"step_count": "Step Count",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"process_step": {
"title": "Process Steps",
"search": {
"process_id": "Process",
"process_id_placeholder": "Please select process",
"step_code": "Step Code",
"step_code_placeholder": "Please enter step code",
"step_name": "Step Name",
"step_name_placeholder": "Please enter step name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"process_id": "Process",
"process_id_placeholder": "Please select process",
"step_code_placeholder": "Please enter step code",
"step_name_placeholder": "Please enter step name",
"description": "Step Description",
"description_placeholder": "Please enter step description"
},
"columns": {
"process_name": "Process",
"step_code": "Step Code",
"step_name": "Step Name",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"step_status": {
"title": "Step Status",
"search": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"status_code": "Status Code",
"status_code_placeholder": "Please enter status code",
"status_name": "Status Name",
"status_name_placeholder": "Please enter status name",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"status_code_placeholder": "Please enter status code",
"status_name_placeholder": "Please enter status name",
"description": "Status Description",
"description_placeholder": "Please enter status description"
},
"columns": {
"step_name": "Step",
"status_code": "Status Code",
"status_name": "Status Name",
"is_initial": "Initial",
"is_final": "Final",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"step_relation": {
"title": "Step Relation",
"search": {
"from_step_id": "From Step",
"from_step_id_placeholder": "Please select from step",
"to_step_id": "To Step",
"to_step_id_placeholder": "Please select to step",
"relation_type": "Relation Type",
"relation_type_placeholder": "Please select relation type",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"from_step_id": "From Step",
"from_step_id_placeholder": "Please select from step",
"to_step_id": "To Step",
"to_step_id_placeholder": "Please select to step",
"flow_condition": "Flow Condition",
"flow_condition_placeholder": "Please enter JSON flow condition",
"description": "Relation Description",
"description_placeholder": "Please enter relation description"
},
"columns": {
"from_step_name": "From Step",
"to_step_name": "To Step",
"relation_type": "Relation Type",
"priority": "Priority",
"flow_condition": "Flow Condition",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"status_transition": {
"title": "Status Transition",
"search": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"trigger_event": "Trigger Event",
"trigger_event_placeholder": "Please select trigger event",
"status": "Status",
"status_placeholder": "Please select status"
},
"form": {
"step_id": "Step",
"step_id_placeholder": "Please select step",
"from_status_id": "From Status",
"from_status_id_placeholder": "Please select from status",
"to_status_id": "To Status",
"to_status_id_placeholder": "Please select to status",
"trigger_event_placeholder": "Please select trigger event",
"side_effect_type": "Side Effect Type",
"side_effect_type_placeholder": "Please select side effect type",
"event_config": "Event Config",
"event_config_placeholder": "Please enter JSON event config",
"side_effect_config": "Side Effect Config",
"side_effect_config_placeholder": "Please enter JSON side effect config",
"description": "Transition Description",
"description_placeholder": "Please enter transition description"
},
"columns": {
"step_name": "Step",
"from_status_name": "From Status",
"to_status_name": "To Status",
"trigger_event": "Trigger Event",
"transition_type": "Transition Type",
"auto_trigger": "Auto Trigger",
"priority": "Priority",
"sort": "Sort",
"status": "Status",
"created_at": "Created At"
}
},
"transition_log": {
"title": "Transition Log",
"search": {
"work_order_no": "Work Order No.",
"work_order_no_placeholder": "Please enter work order no",
"step_id": "Step",
"step_id_placeholder": "Please select step",
"trigger_event": "Trigger Event",
"trigger_event_placeholder": "Please select trigger event",
"trigger_user": "Trigger User",
"trigger_user_placeholder": "Please enter trigger user",
"is_success": "Result",
"is_success_placeholder": "Please select result"
},
"columns": {
"work_order_no": "Work Order No",
"step_name": "Step",
"from_status_name": "From Status",
"to_status_name": "To Status",
"transition_event": "Transition Rule",
"trigger_event": "Trigger Event",
"trigger_user": "Trigger User",
"trigger_ip": "Trigger IP",
"is_auto": "Auto Trigger",
"is_success": "Result",
"duration": "Duration(ms)",
"trigger_data": "Trigger Data",
"error_message": "Error Message",
"remark": "Remark",
"created_name": "Created By",
"created_at": "Created At"
}
},
"config": {
"title": "Production Config"
},
"scan_data": {
"title": "Scan Data"
},
"exception_data": {
"title": "Exception Records"
},
"exception_setting": {
"title": "Exception Settings"
},
"exception_analysis": {
"title": "Exception Analysis"
}
}
}
}
@@ -0,0 +1,217 @@
{
"produce": {
"common": {
"columns": {
"index": "序号"
}
},
"modules": {
"process": {
"title": "工序管理",
"search": {
"process_code": "工序编码",
"process_code_placeholder": "请输入工序编码",
"process_name": "工序名称",
"process_name_placeholder": "请输入工序名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"process_code_placeholder": "请输入工序编码",
"process_name_placeholder": "请输入工序名称",
"description": "工序描述",
"description_placeholder": "请输入工序描述"
},
"columns": {
"process_code": "工序编码",
"process_name": "工序名称",
"step_count": "步骤数",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"process_step": {
"title": "工序步骤",
"search": {
"process_id": "所属工序",
"process_id_placeholder": "请选择所属工序",
"step_code": "步骤编码",
"step_code_placeholder": "请输入步骤编码",
"step_name": "步骤名称",
"step_name_placeholder": "请输入步骤名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"process_id": "所属工序",
"process_id_placeholder": "请选择所属工序",
"step_code_placeholder": "请输入步骤编码",
"step_name_placeholder": "请输入步骤名称",
"description": "步骤描述",
"description_placeholder": "请输入步骤描述"
},
"columns": {
"process_name": "所属工序",
"step_code": "步骤编码",
"step_name": "步骤名称",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"step_status": {
"title": "步骤状态",
"search": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"status_code": "状态编码",
"status_code_placeholder": "请输入状态编码",
"status_name": "状态名称",
"status_name_placeholder": "请输入状态名称",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"status_code_placeholder": "请输入状态编码",
"status_name_placeholder": "请输入状态名称",
"description": "状态描述",
"description_placeholder": "请输入状态描述"
},
"columns": {
"step_name": "所属步骤",
"status_code": "状态编码",
"status_name": "状态名称",
"is_initial": "初始状态",
"is_final": "终态",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"step_relation": {
"title": "步骤关系",
"search": {
"from_step_id": "源步骤",
"from_step_id_placeholder": "请选择源步骤",
"to_step_id": "目标步骤",
"to_step_id_placeholder": "请选择目标步骤",
"relation_type": "关系类型",
"relation_type_placeholder": "请选择关系类型",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"from_step_id": "源步骤",
"from_step_id_placeholder": "请选择源步骤",
"to_step_id": "目标步骤",
"to_step_id_placeholder": "请选择目标步骤",
"flow_condition": "流转条件",
"flow_condition_placeholder": "请输入 JSON 格式流转条件",
"description": "关系描述",
"description_placeholder": "请输入关系描述"
},
"columns": {
"from_step_name": "源步骤",
"to_step_name": "目标步骤",
"relation_type": "关系类型",
"priority": "优先级",
"flow_condition": "流转条件",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"status_transition": {
"title": "状态转换",
"search": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"trigger_event": "触发事件",
"trigger_event_placeholder": "请选择触发事件",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"from_status_id": "源状态",
"from_status_id_placeholder": "请选择源状态",
"to_status_id": "目标状态",
"to_status_id_placeholder": "请选择目标状态",
"trigger_event_placeholder": "请选择触发事件",
"side_effect_type": "副作用类型",
"side_effect_type_placeholder": "请选择副作用类型",
"event_config": "事件配置",
"event_config_placeholder": "请输入 JSON 格式事件配置",
"side_effect_config": "副作用配置",
"side_effect_config_placeholder": "请输入 JSON 格式副作用配置",
"description": "转换描述",
"description_placeholder": "请输入转换描述"
},
"columns": {
"step_name": "所属步骤",
"from_status_name": "源状态",
"to_status_name": "目标状态",
"trigger_event": "触发事件",
"transition_type": "转换类型",
"auto_trigger": "自动触发",
"priority": "优先级",
"sort": "排序",
"status": "状态",
"created_at": "创建时间"
}
},
"transition_log": {
"title": "转换日志",
"search": {
"work_order_no": "工单编号",
"work_order_no_placeholder": "请输入工单编号",
"step_id": "所属步骤",
"step_id_placeholder": "请选择所属步骤",
"trigger_event": "触发事件",
"trigger_event_placeholder": "请选择触发事件",
"trigger_user": "触发用户",
"trigger_user_placeholder": "请输入触发用户",
"is_success": "执行结果",
"is_success_placeholder": "请选择执行结果"
},
"columns": {
"work_order_no": "工单编号",
"step_name": "所属步骤",
"from_status_name": "源状态",
"to_status_name": "目标状态",
"transition_event": "转换规则",
"trigger_event": "触发事件",
"trigger_user": "触发用户",
"trigger_ip": "触发IP",
"is_auto": "自动触发",
"is_success": "执行结果",
"duration": "耗时(ms)",
"trigger_data": "触发数据",
"error_message": "错误信息",
"remark": "备注",
"created_name": "创建人",
"created_at": "创建时间"
}
},
"config": {
"title": "生产配置"
},
"scan_data": {
"title": "扫码数据"
},
"exception_data": {
"title": "异常数据"
},
"exception_setting": {
"title": "异常设置"
},
"exception_analysis": {
"title": "异常数据分析"
}
}
}
}
@@ -0,0 +1,134 @@
<?php
/**
* 后台菜单配置
* 安装时会自动加载
*/
return [
[
'name' => '生产管理',
'code' => 'produce',
'app' => 'produce',
'path' => '/produce',
'icon' => 'plugin',
'children' => [
[
'name' => '生产配置',
'code' => 'produce:config:index',
'app' => 'produce',
'path' => '/produce/config',
'component' => '/produce/config/index',
],
[
'name' => '工序管理',
'code' => 'produce:process:index',
'app' => 'produce',
'path' => '/produce/process',
'component' => '/produce/process/index',
],
[
'name' => '工序步骤',
'code' => 'produce:process-step:index',
'app' => 'produce',
'path' => '/produce/process-step',
'component' => '/produce/process-step/index',
],
[
'name' => '步骤状态',
'code' => 'produce:step-status:index',
'app' => 'produce',
'path' => '/produce/step-status',
'component' => '/produce/step-status/index',
],
[
'name' => '步骤关系',
'code' => 'produce:step-relation:index',
'app' => 'produce',
'path' => '/produce/step-relation',
'component' => '/produce/step-relation/index',
],
[
'name' => '状态转换',
'code' => 'produce:status-transition:index',
'app' => 'produce',
'path' => '/produce/status-transition',
'component' => '/produce/status-transition/index',
],
[
'name' => '整机数据',
'code' => 'produce:product-record:index',
'app' => 'produce',
'path' => '/produce/product-record',
'component' => '/produce/product-record/index',
],
[
'name' => '扫码数据',
'code' => 'produce:scan-data:index',
'app' => 'produce',
'path' => '/produce/scan-data',
'component' => '/produce/scan-data/index',
],
[
'name' => '转换日志',
'code' => 'produce:transition-log:index',
'app' => 'produce',
'path' => '/produce/transition-log',
'component' => '/produce/transition-log/index',
],
[
'name' => '异常管理',
'code' => 'produce:exception:index',
'app' => 'produce',
'path' => '/produce/exception',
'icon' => 'warning',
'children' => [
[
'name' => '异常数据',
'code' => 'produce:exception-data:list',
'app' => 'produce',
'path' => '/produce/exception-data',
'component' => '/produce/exception/data/index',
],
[
'name' => '异常设置',
'code' => 'produce:exception-setting:list',
'app' => 'produce',
'path' => '/produce/exception-setting',
'component' => '/produce/exception/setting/index',
],
[
'name' => '数据分析',
'code' => 'produce:exception-analysis:read',
'app' => 'produce',
'path' => '/produce/exception-analysis',
'component' => '/produce/exception/analysis/index',
],
],
],
[
'name' => '数据统计',
'code' => 'produce:statistics:index',
'app' => 'produce',
'path' => '/produce/statistics',
'icon' => 'bar-chart-2',
'children' => [
[
'name' => '月工作统计',
'code' => 'produce:statistics:monthly',
'app' => 'produce',
'path' => '/produce/statistics/monthly',
'component' => '/produce/statistics/monthly/index',
],
[
'name' => '日工作统计',
'code' => 'produce:statistics:daily',
'app' => 'produce',
'path' => '/produce/statistics/daily',
'component' => '/produce/statistics/daily/index',
],
],
],
],
],
];
@@ -0,0 +1,215 @@
import { AppRouteRecord } from "@/types/router";
import { RoutesAlias } from "@/router/routesAlias";
export default {
path: "/produce",
name: "Produce",
component: RoutesAlias.Layout,
meta: {
title: "生产管理",
icon: "ri:plugin-line",
module: "produce",
},
children: [
{
path: "material-category",
name: "ProduceMaterialCategory",
component: "/produce/material-category/index",
meta: {
title: "物料分类",
icon: "ri:folder-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "material",
name: "ProduceMaterial",
component: "/produce/material/index",
meta: {
title: "物料库",
icon: "ri:archive-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "config",
name: "ProduceConfig",
component: "/produce/config/index",
meta: {
title: "生产配置",
icon: "ri:settings-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "process",
name: "ProduceProcess",
component: "/produce/process/index",
meta: {
title: "工序管理",
icon: "ri:stack-line",
keepAlive: false,
module: "produce",
},
},
{
path: "process-step",
name: "ProduceProcessStep",
component: "/produce/process-step/index",
meta: {
title: "工序步骤",
icon: "ri:list-check-3",
keepAlive: false,
module: "produce",
},
},
{
path: "step-status",
name: "ProduceStepStatus",
component: "/produce/step-status/index",
meta: {
title: "步骤状态",
icon: "ri:flag-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "step-relation",
name: "ProduceStepRelation",
component: "/produce/step-relation/index",
meta: {
title: "步骤关系",
icon: "ri:git-branch-line",
keepAlive: false,
module: "produce",
},
},
{
path: "status-transition",
name: "ProduceStatusTransition",
component: "/produce/status-transition/index",
meta: {
title: "状态转换",
icon: "ri:share-forward-line",
keepAlive: false,
module: "produce",
},
},
{
path: "product-record",
name: "ProduceProductRecord",
component: "/produce/product-record/index",
meta: {
title: "整机数据",
icon: "ri-product-hunt-line",
keepAlive: false,
module: "produce",
},
},
{
path: "scan-data",
name: "ProduceScanData",
component: "/produce/scan-data/index",
meta: {
title: "扫码数据",
icon: "ri:qr-scan-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "transition-log",
name: "ProduceTransitionLog",
component: "/produce/transition-log/index",
meta: {
title: "转换日志",
icon: "ri:file-list-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "repair",
name: "ProduceRepair",
meta: {
title: "维修管理",
icon: "ri:tools-line",
keepAlive: false,
module: "produce",
},
children: [
{
path: "repair-data",
name: "ProduceRepairData",
component: "/produce/repair/repair-data/index",
meta: {
title: "维修数据",
icon: "ri:file-list-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "repair-setting",
name: "ProduceRepairSetting",
component: "/produce/repair/repair-setting/index",
meta: {
title: "维修设置",
icon: "ri:settings-3-line",
keepAlive: false,
module: "produce",
},
},
{
path: "hardware-fault-description",
name: "ProduceHardwareFaultDescription",
component: "/produce/repair/hardware-fault-description/index",
meta: {
title: "硬件故障描述",
icon: "ri:tools-line",
keepAlive: false,
module: "produce",
},
},
],
},
{
path: "statistics",
name: "ProduceStatistics",
meta: {
title: "数据统计",
icon: "ri:bar-chart-2-line",
keepAlive: false,
module: "produce",
},
children: [
{
path: "monthly",
name: "ProduceMonthlyWorkStatistics",
component: "/produce/statistics/monthly/index",
meta: {
title: "月工作统计",
icon: "ri:calendar-2-line",
keepAlive: false,
module: "produce",
},
},
{
path: "daily",
name: "ProduceDailyWorkStatistics",
component: "/produce/statistics/daily/index",
meta: {
title: "日工作统计",
icon: "ri:time-line",
keepAlive: false,
module: "produce",
},
},
],
},
],
} as AppRouteRecord;
@@ -0,0 +1,52 @@
<template>
<div class="page-content" style="margin-bottom: 20px">
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="item in settingList" :key="item.key" :name="item.key" :label="item.name">
<component :is="item.component" :key="item.loadKey" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="tsx">
import { defineAsyncComponent, markRaw, ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const activeName = ref('basic-setting')
const handleClick = (tab: TabsPaneContext) => {
const targetKey = String(tab.props.name)
settingList.value.forEach((item) => {
if (item.key === targetKey) {
item.loadKey += 1
}
})
}
const settingList = ref([
{
component: markRaw(defineAsyncComponent(() => import('./modules/basic-setting.vue'))),
key: 'basic-setting',
name: '基础设置',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
key: 'step-rule',
name: '步骤规则',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/specific-rule.vue'))),
key: 'specific-rule',
name: '特定规则',
loadKey: 0
},
{
component: markRaw(defineAsyncComponent(() => import('./modules/erp-integration.vue'))),
key: 'erp-integration',
name: 'ERP 集成',
loadKey: 0
}
])
</script>
@@ -0,0 +1,175 @@
<template>
<div>
<BasicForm />
<el-button class="site-sumbit" type="primary" @click="onSubmit">保存配置</el-button>
</div>
</template>
<script setup lang="tsx">
import { useForm } from '@/components/form'
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { ProduceConfigApi } from '@/apps/produce/api'
const toBoolean = (value: unknown) => value === 1 || value === '1' || value === true
const normalizeData = (data: Record<string, any>) => {
return {
...data,
global_allow_repeat: toBoolean(data.global_allow_repeat) ? 1 : 0,
recent_records_today_only: toBoolean(data.recent_records_today_only) ? 1 : 0
}
}
const [BasicForm, formApi] = useForm({
labelPosition: 'right',
labelWidth: '160px',
inline: false,
schema: [
{
prop: 'order_no_regex',
label: '订单号验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入订单号验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'order_no_min_length',
label: '订单号最小长度',
component: 'InputNumber',
defaultValue: 8,
componentProps: {
min: 1,
max: 64,
class: 'w-full'
}
},
{
prop: 'process_order_no_regex',
label: '加工单号验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入加工单号验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'user_code_regex',
label: '用户编码验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入用户编码验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'product_code_regex',
label: '商品编码验证正则',
component: 'Input',
componentProps: {
placeholder: '请输入商品编码验证正则',
clearable: true,
style: { width: '50%' }
}
},
{
prop: 'product_code_min_length',
label: '商品编码最小长度',
component: 'InputNumber',
defaultValue: 6,
componentProps: {
min: 1,
max: 64,
class: 'w-full'
}
},
{
prop: 'product_code_max_length',
label: '商品编码最大长度',
component: 'InputNumber',
defaultValue: 20,
componentProps: {
min: 1,
max: 128,
class: 'w-full'
}
},
{
prop: 'global_allow_repeat',
label: '全局允许重复',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 }
]
}
},
{
prop: 'repeat_scan_interval',
label: '重复扫码最小间隔(秒)',
component: 'InputNumber',
defaultValue: 0,
componentProps: {
min: 0,
max: 86400,
class: 'w-full'
}
},
{
prop: 'recent_records_today_only',
label: '最近记录只显示当日',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 }
]
}
}
]
})
async function retrieveData() {
const record = (await ProduceConfigApi.info({ group_code: 'basic_setting' })) as Record<string, any>
await formApi.setValues(normalizeData(record || {}))
}
async function onSubmit() {
const values = await formApi.getValues()
await ProduceConfigApi.save({
group_code: 'basic_setting',
configs: [
{ code: 'order_no_regex', name: '订单号验证正则', content: values.order_no_regex ?? '' },
{ code: 'order_no_min_length', name: '订单号最小长度', content: values.order_no_min_length ?? 8 },
{ code: 'process_order_no_regex', name: '加工单号验证正则', content: values.process_order_no_regex ?? '' },
{ code: 'user_code_regex', name: '用户编码验证正则', content: values.user_code_regex ?? '' },
{ code: 'product_code_regex', name: '商品编码验证正则', content: values.product_code_regex ?? '' },
{ code: 'product_code_min_length', name: '商品编码最小长度', content: values.product_code_min_length ?? 6 },
{ code: 'product_code_max_length', name: '商品编码最大长度', content: values.product_code_max_length ?? 20 },
{ code: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) },
{ code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 0 },
{ code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) }
]
})
ElMessage.success('保存成功')
retrieveData()
}
onMounted(() => {
retrieveData()
})
</script>
<style lang="scss">
.site-sumbit {
margin-left: 160px;
}
</style>
@@ -0,0 +1,207 @@
<template>
<div>
<BasicForm />
<el-button class="site-sumbit" type="primary" @click="onSubmit">保存配置</el-button>
</div>
</template>
<script setup lang="tsx">
import { useForm } from '@/components/form'
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { ProduceConfigApi } from '@/apps/produce/api'
const switchKeys = [
'enable_erp_integration',
'validate_erp_order_on_entry',
'validate_on_every_scan',
'skip_order_status_check',
'enable_order_local_cache',
'show_order_items_detail',
'show_order_logistics_info',
'show_data_source_label',
]
const toBoolean = (value: unknown) => value === 1 || value === '1' || value === true
const yesNoOptions = [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
]
const [BasicForm, formApi] = useForm({
labelPosition: 'right',
labelWidth: '180px',
inline: false,
schema: [
{
prop: 'enable_erp_integration',
label: '启用ERP集成',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'validate_erp_order_on_entry',
label: '录入验证ERP订单',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'validate_on_every_scan',
label: '每次扫码都验证',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'skip_order_status_check',
label: '跳过订单状态检查',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'allowed_order_statuses',
label: '允许录入的订单状态',
component: 'Select',
defaultValue: ['Delivering'],
componentProps: {
multiple: true,
clearable: true,
placeholder: '请选择允许录入的订单状态',
options: [
{ label: '发货中', value: 'Delivering' },
{ label: '已付款待审核', value: 'WaitConfirm' },
{ label: '已客审待财审', value: 'WaitFConfirm' },
{ label: '等供销商/外仓发货', value: 'WaitOuterSent' },
{ label: '已发货', value: 'Sent' },
],
},
},
{
prop: 'order_validation_failure_action',
label: '订单验证失败处理',
component: 'RadioGroup',
defaultValue: 'block',
componentProps: {
options: [
{ label: '阻止录入', value: 'block' },
{ label: '警告但允许录入', value: 'warn' },
{ label: '仅记录日志', value: 'log' },
],
},
},
{
prop: 'order_refresh_strategy',
label: '订单刷新策略',
component: 'RadioGroup',
defaultValue: 'smart',
componentProps: {
options: [
{ label: '永不刷新', value: 'never' },
{ label: '智能刷新', value: 'smart' },
{ label: '总是刷新', value: 'always' },
],
},
},
{
prop: 'order_cache_ttl',
label: '缓存有效期(分钟)',
component: 'InputNumber',
defaultValue: 30,
componentProps: {
min: 1,
max: 1440,
class: 'w-full',
},
},
{
prop: 'enable_order_local_cache',
label: '订单数据本地缓存',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_order_items_detail',
label: '显示订单商品明细',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_order_logistics_info',
label: '显示订单物流信息',
component: 'RadioGroup',
defaultValue: 0,
componentProps: {
options: yesNoOptions,
},
},
{
prop: 'show_data_source_label',
label: '显示数据来源标识',
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: yesNoOptions,
},
},
],
})
async function retrieveData() {
const record = ((await ProduceConfigApi.info({ group_code: 'erp_integration' })) || {}) as Record<string, any>
switchKeys.forEach((key) => {
record[key] = toBoolean(record[key]) ? 1 : 0
})
if (typeof record.allowed_order_statuses === 'string') {
try {
record.allowed_order_statuses = JSON.parse(record.allowed_order_statuses)
} catch {
record.allowed_order_statuses = ['Delivering']
}
}
await formApi.setValues(record)
}
async function onSubmit() {
const values = await formApi.getValues()
await ProduceConfigApi.save({
group_code: 'erp_integration',
configs: [
...switchKeys.map((key) => ({ code: key, name: key, content: Number(values[key] ?? 0) })),
{ code: 'allowed_order_statuses', name: 'allowed_order_statuses', content: JSON.stringify(values.allowed_order_statuses || []) },
{ code: 'order_validation_failure_action', name: 'order_validation_failure_action', content: values.order_validation_failure_action || 'block' },
{ code: 'order_refresh_strategy', name: 'order_refresh_strategy', content: values.order_refresh_strategy || 'smart' },
{ code: 'order_cache_ttl', name: 'order_cache_ttl', content: values.order_cache_ttl ?? 30 },
],
})
ElMessage.success('保存成功')
retrieveData()
}
onMounted(() => {
retrieveData()
})
</script>
<style lang="scss">
.site-sumbit {
margin-left: 180px;
}
</style>
@@ -0,0 +1,324 @@
<template>
<div class="produce-specific-rule-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud, type CrudSchema } from '@/components/crud'
import { ProduceSpecificRuleService } from '@/apps/produce/api'
defineOptions({ name: 'ProduceSpecificRuleList' })
const normalizeStepIds = (value: unknown) => {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const applyModeOptions = [
{ label: '订单模式', value: 'order' },
{ label: '整机模式', value: 'machine' },
{ label: '全部模式', value: 'both' },
]
const matchFieldOptions = [
{ label: '订单号', value: 'order_no' },
{ label: '加工单号', value: 'process_order_no' },
{ label: '商品编码', value: 'product_code' },
{ label: '用户编码', value: 'user_code' },
]
const crudSchema = (): CrudSchema => {
return {
api: ProduceSpecificRuleService.list,
dialogTitle: '特定规则',
crudApi: {
add: (params: any) => ProduceSpecificRuleService.create({
...params,
apply_mode: params?.apply_mode || 'order',
match_field: params?.match_field || 'order_no',
step_ids: normalizeStepIds(params?.step_ids),
}),
edit: (params: any) => ProduceSpecificRuleService.update({
...params,
apply_mode: params?.apply_mode || 'order',
match_field: params?.match_field || 'order_no',
step_ids: normalizeStepIds(params?.step_ids),
}),
remove: ProduceSpecificRuleService.remove,
view: async (id: string | number) => {
const res = await ProduceSpecificRuleService.get(id)
return {
...res,
apply_mode: res?.apply_mode || 'order',
match_field: res?.match_field || 'order_no',
step_ids: normalizeStepIds(res?.step_ids),
}
},
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:specific-rule:create',
editAuth: 'produce:specific-rule:update',
removeAuth: 'produce:specific-rule:delete',
viewAuth: 'produce:specific-rule:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_rule_name',
label: '规则名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规则名称',
},
colSpan: 8,
},
{
prop: 'apply_mode',
label: '适用模式',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择适用模式',
options: applyModeOptions,
},
colSpan: 8,
},
{
prop: 'rule_type',
label: '规则类型',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择规则类型',
options: [
{ label: '允许重复', value: 1 },
{ label: '禁止重复', value: 2 },
],
},
colSpan: 8,
},
{
prop: 'is_enabled',
label: '状态',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择状态',
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'rule_name', label: '规则名称', minWidth: 180 },
{
prop: 'apply_mode',
label: '适用模式',
minWidth: 120,
formatter: (row: any) => (row.apply_mode === 'machine' ? '整机模式' : row.apply_mode === 'both' ? '全部模式' : '订单模式'),
},
{
prop: 'match_field',
label: '匹配字段',
minWidth: 120,
formatter: (row: any) => (row.match_field === 'process_order_no' ? '加工单号' : row.match_field === 'product_code' ? '商品编码' : row.match_field === 'user_code' ? '用户编码' : '订单号'),
},
{ prop: 'rule_pattern', label: '正则表达式', minWidth: 220 },
{
prop: 'rule_type',
label: '规则类型',
minWidth: 120,
formatter: (row: any) => (row.rule_type === 2 ? '禁止重复' : '允许重复'),
},
{
prop: 'step_ids',
label: '适用步骤',
minWidth: 140,
formatter: (row: any) => {
const value = row.step_ids
if (!value || (Array.isArray(value) && value.length === 0)) return '全部步骤'
if (typeof value === 'string') {
try {
const list = JSON.parse(value)
return Array.isArray(list) && list.length > 0 ? `已指定 ${list.length} 个步骤` : '全部步骤'
} catch {
return '全部步骤'
}
}
return Array.isArray(value) && value.length > 0 ? `已指定 ${value.length} 个步骤` : '全部步骤'
},
},
{
prop: 'is_enabled',
label: '状态',
width: 100,
formatter: (row: any) => (row.is_enabled === 0 ? '禁用' : '启用'),
},
{ prop: 'sort', label: '排序', width: 100 },
{ prop: 'remark', label: '备注', minWidth: 180 },
],
dialogProps: {
width: '820px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'rule_name',
label: '规则名称',
componentProps: {
placeholder: '请输入规则名称',
maxlength: 255,
},
rules: [{ required: true, message: '请输入规则名称' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'apply_mode',
label: '适用模式',
defaultValue: 'order',
componentProps: {
options: applyModeOptions,
},
rules: [{ required: true, message: '请选择适用模式' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'rule_type',
label: '规则类型',
defaultValue: 1,
componentProps: {
options: [
{ label: '允许重复', value: 1 },
{ label: '禁止重复', value: 2 },
],
},
rules: [{ required: true, message: '请选择规则类型' }],
colSpan: 12,
},
{
component: 'Select',
prop: 'match_field',
label: '匹配字段',
defaultValue: 'order_no',
componentProps: {
placeholder: '请选择匹配字段',
options: matchFieldOptions,
},
rules: [{ required: true, message: '请选择匹配字段' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'rule_pattern',
label: '正则表达式',
componentProps: {
placeholder: '请输入正则表达式,例如:^[A-Z0-9]{8,20}$',
},
rules: [{ required: true, message: '请输入正则表达式' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'step_ids',
label: '适用步骤',
defaultValue: [],
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
multiple: true,
clearable: true,
placeholder: '请选择适用步骤(不选则全部步骤)',
},
ifDetail: false,
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'is_enabled',
label: '是否启用',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: '排序',
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-specific-rule-page {
height: 100%;
}
</style>
@@ -0,0 +1,237 @@
<template>
<div class="produce-step-rule-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud, type CrudSchema } from '@/components/crud'
import { ProduceProcessStepService } from '@/apps/produce/api'
const normalizeArray = (value: unknown) => {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const formatPrerequisiteMode = (value: string) => {
const modes: Record<string, string> = {
all: '全部完成',
any: '任意完成',
none: '无需验证',
}
return modes[value] || value || '全部完成'
}
const formatYesNo = (value: unknown) => Number(value ?? 0) === 1 ? '是' : '否'
const formatPrerequisiteSteps = (row: any) => {
if (Array.isArray(row.prerequisite_steps_names) && row.prerequisite_steps_names.length > 0) {
return row.prerequisite_steps_names.join(', ')
}
const steps = normalizeArray(row.prerequisite_steps)
return steps.length > 0 ? `已配置 ${steps.length} 个步骤` : '无'
}
const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessStepService.list,
dialogTitle: '步骤规则',
editButtonText: '配置规则',
crudApi: {
edit: (params: any) => ProduceProcessStepService.update(params),
view: async (id: string | number) => {
const res = await ProduceProcessStepService.get(id)
return {
...res,
prerequisite_steps: normalizeArray(res?.prerequisite_steps),
prerequisite_mode: res?.prerequisite_mode || 'all',
prerequisite_require_final: Number(res?.prerequisite_require_final ?? 1),
allow_repeat_scan: Number(res?.allow_repeat_scan ?? 0),
repeat_scan_statuses: normalizeArray(res?.repeat_scan_statuses),
}
},
},
useCrud: true,
hasAdd: false,
hasEdit: true,
hasRemove: false,
hasView: false,
editAuth: 'produce:process-step:update',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'process_id',
label: '所属工序',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择所属工序',
},
colSpan: 8,
},
{
prop: 'LIKE_step_name',
label: '步骤名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入步骤名称',
},
colSpan: 8,
},
],
},
columns: [
{ type: 'index', label: '序号', width: 70 },
{ prop: 'process.process_name', label: '所属工序', minWidth: 160 },
{ prop: 'step_name', label: '步骤名称', minWidth: 160 },
{ prop: 'step_code', label: '步骤编码', minWidth: 130 },
{
prop: 'prerequisite_steps',
label: '前置必要步骤',
minWidth: 180,
formatter: formatPrerequisiteSteps,
},
{
prop: 'prerequisite_mode',
label: '前置验证模式',
minWidth: 130,
formatter: (row: any) => formatPrerequisiteMode(row.prerequisite_mode),
},
{
prop: 'prerequisite_require_final',
label: '要求终态',
width: 110,
formatter: (row: any) => formatYesNo(row.prerequisite_require_final ?? 1),
},
{
prop: 'allow_repeat_scan',
label: '允许重复扫码',
width: 130,
formatter: (row: any) => formatYesNo(row.allow_repeat_scan),
},
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '140px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, colSpan: 24 },
{ component: 'Input', prop: 'process_id', label: '工序ID', show: false, colSpan: 24 },
{ component: 'Input', prop: 'step_code', label: '步骤编码', show: false, colSpan: 24 },
{ component: 'Input', prop: 'sort', label: '排序', show: false, colSpan: 24 },
{ component: 'Input', prop: 'status', label: '状态', show: false, colSpan: 24 },
{
component: 'Input',
prop: 'step_name',
label: '步骤名称',
componentProps: {
disabled: true,
},
colSpan: 24,
},
{
component: 'ApiSelect',
prop: 'prerequisite_steps',
label: '前置必要步骤',
defaultValue: [],
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
multiple: true,
clearable: true,
placeholder: '请选择前置必要步骤',
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'prerequisite_mode',
label: '前置步骤验证模式',
defaultValue: 'all',
componentProps: {
options: [
{ label: '全部完成', value: 'all' },
{ label: '任意完成', value: 'any' },
{ label: '无需验证', value: 'none' },
],
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'prerequisite_require_final',
label: '前置步骤要求终态',
defaultValue: 1,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 24,
},
{
component: 'RadioGroup',
prop: 'allow_repeat_scan',
label: '允许重复扫码',
defaultValue: 0,
componentProps: {
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
colSpan: 24,
},
{
component: 'ApiSelect',
prop: 'repeat_scan_statuses',
label: '允许重复扫码的状态',
defaultValue: [],
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select', limit: 1000, status: 1 },
multiple: true,
clearable: true,
placeholder: '请选择允许重复扫码的状态',
},
colSpan: 24,
},
],
},
}
}
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-step-rule-page {
height: 100%;
}
</style>
@@ -0,0 +1,228 @@
<template>
<div class="exception-analysis-page">
<div class="analysis-toolbar">
<el-date-picker
v-model="filters.reported_at_range"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-input v-model="filters.type_code" clearable placeholder="处理类型编码" @keyup.enter="loadData" />
<el-select v-model="filters.status" clearable placeholder="处理状态">
<el-option label="待处理" :value="0" />
<el-option label="处理中" :value="1" />
<el-option label="已处理" :value="2" />
</el-select>
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="metric-grid">
<div class="metric-item"><span>异常总数</span><strong>{{ overview.total }}</strong></div>
<div class="metric-item pending"><span>待处理</span><strong>{{ overview.pending }}</strong></div>
<div class="metric-item processing"><span>处理中</span><strong>{{ overview.processing }}</strong></div>
<div class="metric-item completed"><span>已处理</span><strong>{{ overview.completed }}</strong></div>
</div>
<div class="analysis-grid" v-loading="loading">
<section class="analysis-section">
<h3>处理类型分布</h3>
<el-table :data="overview.by_type" size="small" height="260" empty-text="暂无数据">
<el-table-column prop="name" label="处理类型" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>异常原因排行</h3>
<el-table :data="overview.by_reason" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="异常原因" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>具体问题排行</h3>
<el-table :data="overview.by_issue" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="具体问题" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section full-width">
<h3>每日提报趋势</h3>
<el-table :data="overview.by_date" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="date" label="日期" min-width="180" />
<el-table-column prop="total" label="异常数量" width="130" align="right" />
</el-table>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import request from '@/utils/http'
interface AnalysisItem {
code: string
name: string
total: number
}
interface DateItem {
date: string
total: number
}
interface AnalysisOverview {
total: number
pending: number
processing: number
completed: number
by_type: AnalysisItem[]
by_reason: AnalysisItem[]
by_issue: AnalysisItem[]
by_date: DateItem[]
}
defineOptions({ name: 'ProduceExceptionAnalysis' })
const defaultRange = (): [string, string] => {
const today = new Date()
const start = new Date(today)
start.setDate(today.getDate() - 29)
const format = (date: Date) => date.toISOString().slice(0, 10)
return [format(start), format(today)]
}
const filters = reactive({
reported_at_range: defaultRange() as string[] | undefined,
type_code: '',
status: undefined as number | undefined,
})
const loading = ref(false)
const overview = ref<AnalysisOverview>({
total: 0,
pending: 0,
processing: 0,
completed: 0,
by_type: [],
by_reason: [],
by_issue: [],
by_date: [],
})
async function loadData() {
loading.value = true
try {
overview.value = await request.get<AnalysisOverview>({
url: '/produce/exception-analysis/overview',
params: filters,
})
} finally {
loading.value = false
}
}
function resetFilters() {
filters.reported_at_range = defaultRange()
filters.type_code = ''
filters.status = undefined
loadData()
}
onMounted(loadData)
</script>
<style scoped>
.exception-analysis-page {
min-height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.analysis-toolbar,
.metric-grid,
.analysis-grid {
display: grid;
gap: 14px;
}
.analysis-toolbar {
grid-template-columns: minmax(260px, 1fr) 180px 150px auto auto;
align-items: center;
padding: 14px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 16px 0;
}
.metric-item,
.analysis-section {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-item {
padding: 18px;
}
.metric-item span,
.metric-item strong {
display: block;
}
.metric-item span {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.metric-item strong {
margin-top: 8px;
color: var(--el-text-color-primary);
font-size: 28px;
line-height: 1;
}
.metric-item.pending strong { color: var(--el-color-danger); }
.metric-item.processing strong { color: var(--el-color-warning); }
.metric-item.completed strong { color: var(--el-color-success); }
.analysis-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.analysis-section {
min-width: 0;
padding: 14px;
}
.analysis-section h3 {
margin: 0 0 12px;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.full-width {
grid-column: 1 / -1;
}
@media (max-width: 1100px) {
.analysis-toolbar,
.analysis-grid { grid-template-columns: 1fr 1fr; }
.metric-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
.analysis-toolbar,
.analysis-grid,
.metric-grid { grid-template-columns: 1fr; }
}
</style>
@@ -0,0 +1,22 @@
<template>
<div class="produce-exception-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionDataList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-exception-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,187 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionRecordService } from '@/apps/produce/api'
const statusOptions = [
{ label: '待处理', value: 0 },
{ label: '处理中', value: 1 },
{ label: '已处理', value: 2 }
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionRecordService.list,
dialogTitle: '异常数据',
crudApi: {
add: ProduceExceptionRecordService.create,
edit: ProduceExceptionRecordService.update,
remove: ProduceExceptionRecordService.remove,
view: ProduceExceptionRecordService.get
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-data:create',
editAuth: 'produce:exception-data:update',
removeAuth: 'produce:exception-data:delete',
viewAuth: 'produce:exception-data:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' },
colSpan: 8
},
{
prop: 'type_code',
label: '处理类型',
component: 'Input',
componentProps: { clearable: true, placeholder: '请输入处理类型编码' },
colSpan: 8
},
{
prop: 'status',
label: '处理状态',
component: 'Select',
componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions },
colSpan: 8
},
{
prop: 'reported_at_range',
label: '提报时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
},
colSpan: 12
}
]
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'order_no', label: '单号/条码', minWidth: 160 },
{ prop: 'type_name', label: '处理类型', minWidth: 120 },
{ prop: 'reason_name', label: '异常原因', minWidth: 120 },
{ prop: 'issue_name', label: '具体问题', minWidth: 140 },
{ prop: 'reporter', label: '提报人', minWidth: 110 },
{ prop: 'status_name', label: '处理状态', minWidth: 100 },
{ prop: 'handler', label: '处理人', minWidth: 110 },
{ prop: 'reported_date', label: '提报时间', minWidth: 170 },
{ prop: 'handled_date', label: '处理时间', minWidth: 170 }
],
dialogProps: { width: '820px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Input',
prop: 'order_no',
label: '单号/条码',
componentProps: { placeholder: '请输入单号或条码', maxlength: 100 },
rules: [{ required: true, message: '请输入单号或条码' }],
colSpan: 12
},
{
component: 'Input',
prop: 'user_code',
label: '提报人工号',
componentProps: { placeholder: '请输入提报人工号', maxlength: 50 },
colSpan: 12
},
{
component: 'Input',
prop: 'reporter',
label: '提报人',
componentProps: { placeholder: '请输入提报人', maxlength: 100 },
colSpan: 12
},
{
component: 'Input',
prop: 'type_code',
label: '处理类型编码',
componentProps: { placeholder: '例如 defective_repair', maxlength: 64 },
rules: [{ required: true, message: '请输入处理类型编码' }],
colSpan: 12
},
{
component: 'Input',
prop: 'type_name',
label: '处理类型',
componentProps: { placeholder: '例如 不良维修', maxlength: 100 },
rules: [{ required: true, message: '请输入处理类型' }],
colSpan: 12
},
{
component: 'Input',
prop: 'reason_code',
label: '异常原因编码',
componentProps: { placeholder: '例如 test_defect', maxlength: 64 },
rules: [{ required: true, message: '请输入异常原因编码' }],
colSpan: 12
},
{
component: 'Input',
prop: 'reason_name',
label: '异常原因',
componentProps: { placeholder: '例如 测试不良', maxlength: 100 },
rules: [{ required: true, message: '请输入异常原因' }],
colSpan: 12
},
{
component: 'Input',
prop: 'issue_code',
label: '具体问题编码',
componentProps: { placeholder: '可选', maxlength: 64 },
colSpan: 12
},
{
component: 'Input',
prop: 'issue_name',
label: '具体问题',
componentProps: { placeholder: '可选', maxlength: 100 },
colSpan: 12
},
{
component: 'Select',
prop: 'status',
label: '处理状态',
defaultValue: 0,
componentProps: { options: statusOptions },
colSpan: 12
},
{
component: 'Input',
prop: 'handler',
label: '处理人',
componentProps: { placeholder: '请输入处理人', maxlength: 100 },
colSpan: 12
},
{
component: 'Upload',
prop: 'images',
label: '现场图片',
componentProps: { listType: 'picture-card' },
ifDetail: true,
colSpan: 24
},
{
component: 'Input',
prop: 'remark',
label: '补充说明',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' },
colSpan: 24
}
]
}
})
@@ -0,0 +1,22 @@
<template>
<div class="exception-setting-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionSettingList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.exception-setting-page {
height: 100%;
}
</style>
@@ -0,0 +1,100 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionSettingService } from '@/apps/produce/api'
const settingTypeOptions = [
{ label: '处理类型', value: 'type' },
{ label: '异常原因', value: 'reason' },
{ label: '具体问题', value: 'issue' },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionSettingService.list,
dialogTitle: '异常设置',
crudApi: {
add: ProduceExceptionSettingService.create,
edit: ProduceExceptionSettingService.update,
remove: ProduceExceptionSettingService.remove,
view: ProduceExceptionSettingService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-setting:create',
editAuth: 'produce:exception-setting:update',
removeAuth: 'produce:exception-setting:delete',
viewAuth: 'produce:exception-setting:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword', label: '关键字', component: 'Input',
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
},
{
prop: 'setting_type', label: '设置层级', component: 'Select',
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
},
{
prop: 'is_enabled', label: '启用状态', component: 'Select',
componentProps: {
clearable: true, placeholder: '请选择启用状态',
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
}, colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
{ prop: 'code', label: '编码', minWidth: 150 },
{ prop: 'name', label: '名称', minWidth: 160 },
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
{ prop: 'sort', label: '排序', width: 90 },
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ prop: 'created_name', label: '创建人', minWidth: 110 },
],
dialogProps: { width: '700px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Select', prop: 'setting_type', label: '设置层级',
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
},
{
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
componentProps: {
api: '/produce/exception-setting', requestMethod: 'get',
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
}, colSpan: 12,
},
{
component: 'Input', prop: 'code', label: '设置编码',
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'name', label: '设置名称',
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
},
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
{
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
},
{
component: 'Input', prop: 'remark', label: '备注',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,228 @@
<template>
<div class="exception-analysis-page">
<div class="analysis-toolbar">
<el-date-picker
v-model="filters.reported_at_range"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-input v-model="filters.type_code" clearable placeholder="处理类型编码" @keyup.enter="loadData" />
<el-select v-model="filters.status" clearable placeholder="处理状态">
<el-option label="待处理" :value="0" />
<el-option label="处理中" :value="1" />
<el-option label="已处理" :value="2" />
</el-select>
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="metric-grid">
<div class="metric-item"><span>异常总数</span><strong>{{ overview.total }}</strong></div>
<div class="metric-item pending"><span>待处理</span><strong>{{ overview.pending }}</strong></div>
<div class="metric-item processing"><span>处理中</span><strong>{{ overview.processing }}</strong></div>
<div class="metric-item completed"><span>已处理</span><strong>{{ overview.completed }}</strong></div>
</div>
<div class="analysis-grid" v-loading="loading">
<section class="analysis-section">
<h3>处理类型分布</h3>
<el-table :data="overview.by_type" size="small" height="260" empty-text="暂无数据">
<el-table-column prop="name" label="处理类型" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>异常原因排行</h3>
<el-table :data="overview.by_reason" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="异常原因" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section">
<h3>具体问题排行</h3>
<el-table :data="overview.by_issue" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="排名" width="70" />
<el-table-column prop="name" label="具体问题" min-width="150" />
<el-table-column prop="total" label="数量" width="100" align="right" />
</el-table>
</section>
<section class="analysis-section full-width">
<h3>每日提报趋势</h3>
<el-table :data="overview.by_date" size="small" height="260" empty-text="暂无数据">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="date" label="日期" min-width="180" />
<el-table-column prop="total" label="异常数量" width="130" align="right" />
</el-table>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import request from '@/utils/http'
interface AnalysisItem {
code: string
name: string
total: number
}
interface DateItem {
date: string
total: number
}
interface AnalysisOverview {
total: number
pending: number
processing: number
completed: number
by_type: AnalysisItem[]
by_reason: AnalysisItem[]
by_issue: AnalysisItem[]
by_date: DateItem[]
}
defineOptions({ name: 'ProduceExceptionAnalysis' })
const defaultRange = (): [string, string] => {
const today = new Date()
const start = new Date(today)
start.setDate(today.getDate() - 29)
const format = (date: Date) => date.toISOString().slice(0, 10)
return [format(start), format(today)]
}
const filters = reactive({
reported_at_range: defaultRange() as string[] | undefined,
type_code: '',
status: undefined as number | undefined,
})
const loading = ref(false)
const overview = ref<AnalysisOverview>({
total: 0,
pending: 0,
processing: 0,
completed: 0,
by_type: [],
by_reason: [],
by_issue: [],
by_date: [],
})
async function loadData() {
loading.value = true
try {
overview.value = await request.get<AnalysisOverview>({
url: '/produce/exception-analysis/overview',
params: filters,
})
} finally {
loading.value = false
}
}
function resetFilters() {
filters.reported_at_range = defaultRange()
filters.type_code = ''
filters.status = undefined
loadData()
}
onMounted(loadData)
</script>
<style scoped>
.exception-analysis-page {
min-height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.analysis-toolbar,
.metric-grid,
.analysis-grid {
display: grid;
gap: 14px;
}
.analysis-toolbar {
grid-template-columns: minmax(260px, 1fr) 180px 150px auto auto;
align-items: center;
padding: 14px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 16px 0;
}
.metric-item,
.analysis-section {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.metric-item {
padding: 18px;
}
.metric-item span,
.metric-item strong {
display: block;
}
.metric-item span {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.metric-item strong {
margin-top: 8px;
color: var(--el-text-color-primary);
font-size: 28px;
line-height: 1;
}
.metric-item.pending strong { color: var(--el-color-danger); }
.metric-item.processing strong { color: var(--el-color-warning); }
.metric-item.completed strong { color: var(--el-color-success); }
.analysis-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.analysis-section {
min-width: 0;
padding: 14px;
}
.analysis-section h3 {
margin: 0 0 12px;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.full-width {
grid-column: 1 / -1;
}
@media (max-width: 1100px) {
.analysis-toolbar,
.analysis-grid { grid-template-columns: 1fr 1fr; }
.metric-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
.analysis-toolbar,
.analysis-grid,
.metric-grid { grid-template-columns: 1fr; }
}
</style>
@@ -0,0 +1,22 @@
<template>
<div class="produce-exception-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionDataList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-exception-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,127 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionRecordService } from '@/apps/produce/api'
const statusOptions = [
{ label: '待处理', value: 0 },
{ label: '处理中', value: 1 },
{ label: '已处理', value: 2 },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionRecordService.list,
dialogTitle: '异常数据',
crudApi: {
add: ProduceExceptionRecordService.create,
edit: ProduceExceptionRecordService.update,
remove: ProduceExceptionRecordService.remove,
view: ProduceExceptionRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-data:create',
editAuth: 'produce:exception-data:update',
removeAuth: 'produce:exception-data:delete',
viewAuth: 'produce:exception-data:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' },
colSpan: 8,
},
{
prop: 'type_code',
label: '处理类型',
component: 'Input',
componentProps: { clearable: true, placeholder: '请输入处理类型编码' },
colSpan: 8,
},
{
prop: 'status',
label: '处理状态',
component: 'Select',
componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions },
colSpan: 8,
},
{
prop: 'reported_at_range',
label: '提报时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'order_no', label: '单号/条码', minWidth: 160 },
{ prop: 'type_name', label: '处理类型', minWidth: 120 },
{ prop: 'reason_name', label: '异常原因', minWidth: 120 },
{ prop: 'issue_name', label: '具体问题', minWidth: 140 },
{ prop: 'reporter', label: '提报人', minWidth: 110 },
{ prop: 'status_name', label: '处理状态', minWidth: 100 },
{ prop: 'handler', label: '处理人', minWidth: 110 },
{ prop: 'reported_date', label: '提报时间', minWidth: 170 },
{ prop: 'handled_date', label: '处理时间', minWidth: 170 },
],
dialogProps: { width: '820px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Input', prop: 'order_no', label: '单号/条码',
componentProps: { placeholder: '请输入单号或条码', maxlength: 100 },
rules: [{ required: true, message: '请输入单号或条码' }], colSpan: 12,
},
{ component: 'Input', prop: 'user_code', label: '提报人工号', componentProps: { placeholder: '请输入提报人工号', maxlength: 50 }, colSpan: 12 },
{ component: 'Input', prop: 'reporter', label: '提报人', componentProps: { placeholder: '请输入提报人', maxlength: 100 }, colSpan: 12 },
{
component: 'Input', prop: 'type_code', label: '处理类型编码',
componentProps: { placeholder: '例如 defective_repair', maxlength: 64 },
rules: [{ required: true, message: '请输入处理类型编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'type_name', label: '处理类型',
componentProps: { placeholder: '例如 不良维修', maxlength: 100 },
rules: [{ required: true, message: '请输入处理类型' }], colSpan: 12,
},
{
component: 'Input', prop: 'reason_code', label: '异常原因编码',
componentProps: { placeholder: '例如 test_defect', maxlength: 64 },
rules: [{ required: true, message: '请输入异常原因编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'reason_name', label: '异常原因',
componentProps: { placeholder: '例如 测试不良', maxlength: 100 },
rules: [{ required: true, message: '请输入异常原因' }], colSpan: 12,
},
{ component: 'Input', prop: 'issue_code', label: '具体问题编码', componentProps: { placeholder: '可选', maxlength: 64 }, colSpan: 12 },
{ component: 'Input', prop: 'issue_name', label: '具体问题', componentProps: { placeholder: '可选', maxlength: 100 }, colSpan: 12 },
{ component: 'Select', prop: 'status', label: '处理状态', defaultValue: 0, componentProps: { options: statusOptions }, colSpan: 12 },
{ component: 'Input', prop: 'handler', label: '处理人', componentProps: { placeholder: '请输入处理人', maxlength: 100 }, colSpan: 12 },
{
component: 'Upload', prop: 'images', label: '现场图片', componentProps: { listType: 'picture-card' }, ifDetail: true, colSpan: 24,
},
{
component: 'Input', prop: 'remark', label: '补充说明',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,22 @@
<template>
<div class="exception-setting-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceExceptionSettingList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.exception-setting-page {
height: 100%;
}
</style>
@@ -0,0 +1,100 @@
import { CrudSchema } from '@/components/crud'
import { ProduceExceptionSettingService } from '@/apps/produce/api'
const settingTypeOptions = [
{ label: '处理类型', value: 'type' },
{ label: '异常原因', value: 'reason' },
{ label: '具体问题', value: 'issue' },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceExceptionSettingService.list,
dialogTitle: '异常设置',
crudApi: {
add: ProduceExceptionSettingService.create,
edit: ProduceExceptionSettingService.update,
remove: ProduceExceptionSettingService.remove,
view: ProduceExceptionSettingService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:exception-setting:create',
editAuth: 'produce:exception-setting:update',
removeAuth: 'produce:exception-setting:delete',
viewAuth: 'produce:exception-setting:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword', label: '关键字', component: 'Input',
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
},
{
prop: 'setting_type', label: '设置层级', component: 'Select',
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
},
{
prop: 'is_enabled', label: '启用状态', component: 'Select',
componentProps: {
clearable: true, placeholder: '请选择启用状态',
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
}, colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
{ prop: 'code', label: '编码', minWidth: 150 },
{ prop: 'name', label: '名称', minWidth: 160 },
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
{ prop: 'sort', label: '排序', width: 90 },
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ prop: 'created_name', label: '创建人', minWidth: 110 },
],
dialogProps: { width: '700px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Select', prop: 'setting_type', label: '设置层级',
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
},
{
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
componentProps: {
api: '/produce/exception-setting', requestMethod: 'get',
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
}, colSpan: 12,
},
{
component: 'Input', prop: 'code', label: '设置编码',
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'name', label: '设置名称',
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
},
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
{
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
},
{
component: 'Input', prop: 'remark', label: '备注',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,24 @@
<template>
<div class="produce-material-category-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceMaterialCategoryList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-material-category-page { height: 100%; }
</style>
@@ -0,0 +1,42 @@
import { CrudSchema } from '@/components/crud'
import { ProduceMaterialCategoryService } from '../../../../api'
export const crudSchema = (): CrudSchema => ({
api: ProduceMaterialCategoryService.list,
dialogTitle: '物料分类',
crudApi: {
add: ProduceMaterialCategoryService.create,
edit: ProduceMaterialCategoryService.update,
remove: ProduceMaterialCategoryService.remove,
view: ProduceMaterialCategoryService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:material-category:create',
editAuth: 'produce:material-category:update',
removeAuth: 'produce:material-category:delete',
viewAuth: 'produce:material-category:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [{ prop: 'LIKE_category_name', label: '分类名称', component: 'Input', componentProps: { clearable: true, placeholder: '请输入分类名称' }, colSpan: 8 }],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'category_name', label: '分类名称', minWidth: 220 },
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
],
dialogProps: { width: '520px' },
formSchema: {
showDefaultActions: false,
labelWidth: '90px',
schema: [
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
{ component: 'Input', prop: 'category_name', label: '分类名称', componentProps: { maxlength: 100, placeholder: '请输入分类名称' }, rules: [{ required: true, message: '请输入分类名称' }], colSpan: 24 },
],
},
})
@@ -0,0 +1,49 @@
<template>
<div class="produce-material-page art-full-height">
<div class="material-actions">
<ElUpload :show-file-list="false" accept=".xlsx,.xls,.csv" :http-request="importMaterials">
<ElButton type="primary" plain>Excel 导入</ElButton>
</ElUpload>
<span class="material-hint">表头:分类、商品编码、商品名称;同编码将更新。</span>
</div>
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { ElMessage } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import { useCrud } from '@/components/crud'
import { ProduceMaterialService } from '../../../api'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceMaterialList' })
const [BasicCrud, crudExpose] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
async function importMaterials(options: UploadRequestOptions) {
try {
const formData = new FormData()
formData.append('file', options.file)
const result = await ProduceMaterialService.importExcel(formData)
ElMessage.success(`导入成功:新增 ${result.created} 条,更新 ${result.updated} 条`)
options.onSuccess?.(result)
crudExpose.refreshCreate()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : 'Excel导入失败')
options.onError?.(error as Error)
}
}
</script>
<style scoped>
.produce-material-page { height: 100%; }
.material-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.material-hint { color: var(--el-text-color-secondary); font-size: 13px; }
</style>
@@ -0,0 +1,50 @@
import { CrudSchema } from '@/components/crud'
import { ProduceMaterialService } from '../../../../api'
export const crudSchema = (): CrudSchema => ({
api: ProduceMaterialService.list,
dialogTitle: '物料库',
crudApi: {
add: ProduceMaterialService.create,
edit: ProduceMaterialService.update,
remove: ProduceMaterialService.remove,
view: ProduceMaterialService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
hasExport: true,
addAuth: 'produce:material:create',
editAuth: 'produce:material:update',
removeAuth: 'produce:material:delete',
viewAuth: 'produce:material:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{ prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 },
{ prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, colSpan: 8 },
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'category_name', label: '物料分类', minWidth: 160 },
{ prop: 'product_code', label: '商品编码', minWidth: 180 },
{ prop: 'product_name', label: '商品名称', minWidth: 220 },
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
],
dialogProps: { width: '640px' },
formSchema: {
showDefaultActions: false,
labelWidth: '90px',
schema: [
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
{ component: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, rules: [{ required: true, message: '请选择物料分类' }], colSpan: 12 },
{ component: 'Input', prop: 'product_code', label: '商品编码', componentProps: { maxlength: 100, placeholder: '请输入或扫描商品编码' }, rules: [{ required: true, message: '请输入商品编码' }], colSpan: 12 },
{ component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 },
],
},
})
@@ -0,0 +1,26 @@
<template>
<div class="produce-process-step-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProcessStepList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-process-step-page {
height: 100%;
}
</style>
@@ -0,0 +1,185 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceProcessStepService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessStepService.list,
dialogTitle: $t('produce.modules.process_step.title'),
crudApi: {
add: ProduceProcessStepService.create,
edit: ProduceProcessStepService.update,
remove: ProduceProcessStepService.remove,
view: ProduceProcessStepService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:process-step:create',
editAuth: 'produce:process-step:update',
removeAuth: 'produce:process-step:delete',
viewAuth: 'produce:process-step:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'process_id',
label: $t('produce.modules.process_step.search.process_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.process_step.search.process_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_step_code',
label: $t('produce.modules.process_step.search.step_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process_step.search.step_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_step_name',
label: $t('produce.modules.process_step.search.step_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process_step.search.step_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.process_step.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.process_step.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{
prop: 'process.process_name',
label: $t('produce.modules.process_step.columns.process_name'),
minWidth: 180,
},
{ prop: 'step_code', label: $t('produce.modules.process_step.columns.step_code'), minWidth: 160 },
{ prop: 'step_name', label: $t('produce.modules.process_step.columns.step_name'), minWidth: 180 },
{ prop: 'sort', label: $t('produce.modules.process_step.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.process_step.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.process_step.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '680px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'process_id',
label: $t('produce.modules.process_step.form.process_id'),
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.process_step.form.process_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.process_id_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'step_code',
label: $t('produce.modules.process_step.columns.step_code'),
componentProps: {
placeholder: $t('produce.modules.process_step.form.step_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.step_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'step_name',
label: $t('produce.modules.process_step.columns.step_name'),
componentProps: {
placeholder: $t('produce.modules.process_step.form.step_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.process_step.form.step_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.process_step.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.process_step.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.process_step.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.process_step.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-process-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProcessList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-process-page {
height: 100%;
}
</style>
@@ -0,0 +1,154 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceProcessService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProcessService.list,
dialogTitle: $t('produce.modules.process.title'),
crudApi: {
add: ProduceProcessService.create,
edit: ProduceProcessService.update,
remove: ProduceProcessService.remove,
view: ProduceProcessService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:process:create',
editAuth: 'produce:process:update',
removeAuth: 'produce:process:delete',
viewAuth: 'produce:process:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_process_code',
label: $t('produce.modules.process.search.process_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process.search.process_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_process_name',
label: $t('produce.modules.process.search.process_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.process.search.process_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.process.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.process.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'process_code', label: $t('produce.modules.process.columns.process_code'), minWidth: 160 },
{ prop: 'process_name', label: $t('produce.modules.process.columns.process_name'), minWidth: 180 },
{ prop: 'step_count', label: $t('produce.modules.process.columns.step_count'), width: 120 },
{ prop: 'sort', label: $t('produce.modules.process.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.process.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.process.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '640px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '100px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'process_code',
label: $t('produce.modules.process.columns.process_code'),
componentProps: {
placeholder: $t('produce.modules.process.form.process_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.process.form.process_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'process_name',
label: $t('produce.modules.process.columns.process_name'),
componentProps: {
placeholder: $t('produce.modules.process.form.process_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.process.form.process_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.process.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.process.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.process.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.process.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,22 @@
<template>
<div class="produce-product-record-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceProductRecordList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-product-record-page {
height: 100%;
}
</style>
@@ -0,0 +1,249 @@
import { CrudSchema } from '@/components/crud'
import { ProduceProductRecordService } from '@/apps/produce/api'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceProductRecordService.list,
dialogTitle: '整机数据',
crudApi: {
add: ProduceProductRecordService.create,
edit: ProduceProductRecordService.update,
remove: ProduceProductRecordService.remove,
view: ProduceProductRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:product-record:create',
editAuth: 'produce:product-record:update',
removeAuth: 'produce:product-record:delete',
viewAuth: 'produce:product-record:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入加工单号/商品编码/操作人员/备注',
},
colSpan: 6,
},
{
prop: 'process_id',
label: '工序名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
},
colSpan: 6,
},
{
prop: 'step_id',
label: '步骤名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
},
colSpan: 6,
},
{
prop: 'status_id',
label: '步骤状态',
component: 'ApiSelect',
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
},
colSpan: 6,
},
{
prop: 'scan_result',
label: '扫码结果',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择扫码结果',
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
colSpan: 6,
},
{
prop: 'created_time_range',
label: '创建时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'process_order_no', label: '加工单号', minWidth: 160 },
{ prop: 'product_code', label: '商品编码', minWidth: 160 },
{ prop: 'quantity', label: '数量', width: 100 },
{ prop: 'process_name', label: '工序名称', minWidth: 140 },
{ prop: 'step_name', label: '步骤名称', minWidth: 140 },
{ prop: 'status_name', label: '步骤状态', minWidth: 120 },
{ prop: 'operator', label: '操作人员', minWidth: 120 },
{
prop: 'scan_result',
label: '扫码结果',
width: 100,
formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'),
},
{ prop: 'created_date', label: '创建时间', minWidth: 180 },
{ prop: 'created_name', label: '创建人', minWidth: 120 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'process_order_no',
label: '加工单号',
componentProps: {
placeholder: '请输入加工单号',
maxlength: 100,
},
rules: [{ required: true, message: '请输入加工单号' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'product_code',
label: '商品编码',
componentProps: {
placeholder: '请输入商品编码',
maxlength: 100,
},
rules: [{ required: true, message: '请输入商品编码' }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'quantity',
label: '数量',
defaultValue: 1,
componentProps: {
min: 1,
precision: 0,
},
rules: [{ required: true, message: '请输入数量' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'user_code',
label: '用户编码',
componentProps: {
placeholder: '请输入用户编码',
maxlength: 50,
},
rules: [{ required: true, message: '请输入用户编码' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'operator',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员',
maxlength: 100,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'process_name',
label: '工序名称',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'step_name',
label: '步骤名称',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'status_name',
label: '步骤状态',
componentProps: {
disabled: true,
},
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'scan_result',
label: '扫码结果',
defaultValue: 1,
componentProps: {
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
rules: [{ required: true, message: '请选择扫码结果' }],
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,49 @@
<template>
<div class="produce-hardware-fault-description-page art-full-height">
<div class="hardware-fault-actions">
<ElUpload :show-file-list="false" accept=".xlsx,.xls,.csv" :http-request="importHardwareFaultDescriptions">
<ElButton type="primary" plain>Excel 导入</ElButton>
</ElUpload>
<span class="hardware-fault-hint">表头:分类、描述;同分类同描述将自动跳过并计为更新。</span>
</div>
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { ElMessage } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import { useCrud } from '@/components/crud'
import { ProduceHardwareFaultDescriptionService } from '@/apps/produce/api'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceHardwareFaultDescriptionList' })
const [BasicCrud, crudExpose] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
async function importHardwareFaultDescriptions(options: UploadRequestOptions) {
try {
const formData = new FormData()
formData.append('file', options.file)
const result = await ProduceHardwareFaultDescriptionService.importExcel(formData)
ElMessage.success(`导入成功:新增 ${result.created} 条,更新 ${result.updated} 条`)
options.onSuccess?.(result)
crudExpose.refreshCreate()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : 'Excel导入失败')
options.onError?.(error as Error)
}
}
</script>
<style scoped>
.produce-hardware-fault-description-page { height: 100%; }
.hardware-fault-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.hardware-fault-hint { color: var(--el-text-color-secondary); font-size: 13px; }
</style>
@@ -0,0 +1,135 @@
import { CrudSchema } from "@/components/crud";
import { ProduceHardwareFaultDescriptionService } from "@/apps/produce/api";
const enabledOptions = [
{ label: "启用", value: 1 },
{ label: "停用", value: 0 },
];
export const crudSchema = (): CrudSchema => ({
api: ProduceHardwareFaultDescriptionService.list,
dialogTitle: "硬件故障描述",
crudApi: {
add: ProduceHardwareFaultDescriptionService.create,
edit: ProduceHardwareFaultDescriptionService.update,
remove: ProduceHardwareFaultDescriptionService.remove,
view: ProduceHardwareFaultDescriptionService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: "produce:hardware-fault-description:create",
editAuth: "produce:hardware-fault-description:update",
removeAuth: "produce:hardware-fault-description:delete",
viewAuth: "produce:hardware-fault-description:read",
rowKey: "id",
searchFormSchema: {
labelWidth: "90px",
schema: [
{
prop: "category_id",
label: "物料分类",
component: "ApiSelect",
componentProps: {
api: ProduceHardwareFaultDescriptionService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
},
colSpan: 8,
},
{
prop: "keyword",
label: "关键字",
component: "Input",
componentProps: { clearable: true, placeholder: "故障描述或备注" },
colSpan: 8,
},
{
prop: "is_enabled",
label: "启用状态",
component: "Select",
componentProps: { clearable: true, options: enabledOptions },
colSpan: 8,
},
],
},
columns: [
{ type: "selection" },
{ type: "index", label: "序号", width: 70 },
{ prop: "category_name", label: "物料分类", minWidth: 160 },
{
prop: "description",
label: "硬件故障描述",
minWidth: 300,
showOverflowTooltip: true,
},
{ prop: "use_count", label: "使用次数", width: 100 },
{
prop: "is_enabled",
label: "状态",
width: 90,
formatter: (row: { is_enabled?: number }) =>
row.is_enabled === 0 ? "停用" : "启用",
},
{ prop: "remark", label: "备注", minWidth: 180, showOverflowTooltip: true },
{ prop: "created_date", label: "创建时间", minWidth: 180 },
],
dialogProps: { width: "720px" },
formSchema: {
showDefaultActions: false,
labelWidth: "110px",
schema: [
{
component: "Input",
prop: "id",
label: "ID",
show: false,
ifDetail: true,
colSpan: 24,
},
{
component: "ApiSelect",
prop: "category_id",
label: "物料分类",
componentProps: {
api: ProduceHardwareFaultDescriptionService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
placeholder: "请选择物料分类",
},
rules: [{ required: true, message: "请选择物料分类" }],
colSpan: 12,
},
{
component: "RadioGroup",
prop: "is_enabled",
label: "是否启用",
defaultValue: 1,
componentProps: { options: enabledOptions },
colSpan: 12,
},
{
component: "Input",
prop: "description",
label: "故障描述",
componentProps: {
maxlength: 500,
placeholder: "请输入该物料分类的硬件故障描述",
},
rules: [{ required: true, message: "请输入硬件故障描述" }],
colSpan: 24,
},
{
component: "Input",
prop: "remark",
label: "备注",
componentProps: { type: "textarea", rows: 3, maxlength: 500 },
colSpan: 24,
},
],
},
});
@@ -0,0 +1,22 @@
<template>
<div class="produce-repair-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
defineOptions({ name: "ProduceRepairDataList" });
const [BasicCrud] = useCrud({
...crudSchema(),
});
</script>
<style scoped>
.produce-repair-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,490 @@
import { h } from "vue";
import { CrudSchema } from "@/components/crud";
import { ProduceRepairRecordService } from "@/apps/produce/api";
const formatPartSource = (source: unknown): string => {
const labels: Record<string, string> = {
new: "新料",
old: "售后",
after_sale: "售后",
after_sales: "售后",
"after-sale": "售后",
repair: "返修",
refurbished: "返修",
};
const value = String(source || "");
return labels[value] || value || "-";
};
const parseParts = (parts: Array<Record<string, any>> | string | null | undefined): Array<Record<string, any>> => {
if (Array.isArray(parts)) return parts;
if (typeof parts !== "string" || !parts) return [];
try {
const parsed = JSON.parse(parts);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const formatPartValues = (
parts: Array<Record<string, any>> | string | null | undefined,
getValue: (part: Record<string, any>) => string,
): string =>
parseParts(parts)
.map((part) => getValue(part))
.filter(Boolean)
.join("\n");
const formatPartRows = (
parts: Array<Record<string, any>> | string | null | undefined,
getValue: (part: Record<string, any>) => string,
nowrap = true,
) =>
h(
"div",
parseParts(parts).map((part) =>
h(
"div",
{ style: { lineHeight: "24px", minHeight: "24px", whiteSpace: nowrap ? "nowrap" : "normal" } },
getValue(part) || "-",
),
),
);
const formatPartFaultDescriptions = (part: Record<string, any>): string =>
Array.isArray(part.fault_descriptions)
? part.fault_descriptions
.map((fault: { description?: string }) => fault.description || "")
.filter(Boolean)
.join("、")
: "";
const formatCategoryNames = (row: {
category_names?: string[] | string | null;
category_name?: string | null;
}): string => {
const names = row.category_names;
if (Array.isArray(names)) return names.filter(Boolean).join("、");
if (typeof names === "string" && names) {
try {
const parsed = JSON.parse(names);
if (Array.isArray(parsed)) return parsed.filter(Boolean).join("、");
} catch {
return names;
}
return names;
}
return row.category_name || "";
};
type ResponsibilitySnapshot = {
process_name?: string;
step_name?: string;
operator?: string;
user_code?: string;
};
const parseSnapshots = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): ResponsibilitySnapshot[] => {
if (Array.isArray(snapshots)) return snapshots;
if (typeof snapshots !== "string" || !snapshots) return [];
try {
const parsed = JSON.parse(snapshots);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const formatRepairResult = (repairResult?: string | null): string => {
const labels: Record<string, string> = {
material_defect: "物料不良",
operation_defect: "作业不良",
assembly_mismatch: "搭配错误",
};
return labels[repairResult || ""] || repairResult || "";
};
const formatOperationFaultSteps = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => [item.process_name, item.step_name].filter(Boolean).join(" · "))
.filter(Boolean)
.join("\n");
const formatResponsibleOperators = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => {
const operator = item.operator || item.user_code || "";
const process = [item.process_name, item.step_name].filter(Boolean).join(" · ");
return [operator, process].filter(Boolean).join("|");
})
.filter(Boolean)
.join("\n");
const formatResponsibleOperatorNames = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string =>
parseSnapshots(snapshots)
.map((item) => {
const step = item.step_name || "";
const operator = item.operator || item.user_code || "";
return [step, operator].filter(Boolean).join("-");
})
.filter(Boolean)
.join("\n");
export const crudSchema = (): CrudSchema => ({
api: ProduceRepairRecordService.list,
dialogTitle: "维修数据",
crudApi: {
add: ProduceRepairRecordService.create,
edit: ProduceRepairRecordService.update,
remove: ProduceRepairRecordService.remove,
view: ProduceRepairRecordService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
hasExport: true,
exportFileName: "维修数据",
exportFields: [
{ field: "repair_no", title: "维修单号" },
{ field: "order_product", title: "订单号/商品编码" },
{ field: "exception_type_name", title: "异常分类" },
{ field: "exception_reason", title: "异常原因" },
{ field: "part_category", title: "物料分类" },
{ field: "part_code", title: "物料编码" },
{ field: "part_name", title: "更换备件" },
{ field: "part_fault", title: "故障描述" },
{ field: "part_quantity", title: "数量" },
{ field: "part_source", title: "来源" },
{ field: "repair_result_name", title: "维修判定分类" },
{ field: "responsible_operator_names", title: "责任人员" },
{ field: "operator", title: "维修人员" },
{ field: "repaired_date", title: "维修时间" },
],
addAuth: "produce:repair-record:create",
editAuth: "produce:repair-record:update",
removeAuth: "produce:repair-record:delete",
viewAuth: "produce:repair-record:read",
rowKey: "id",
searchFormSchema: {
labelWidth: "90px",
schema: [
{
prop: "keyword",
label: "关键字",
component: "Input",
componentProps: {
clearable: true,
placeholder: "维修单、订单、商品或故障描述",
},
colSpan: 8,
},
{
prop: "category_id",
label: "物料分类",
component: "ApiSelect",
componentProps: {
api: ProduceRepairRecordService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
},
colSpan: 8,
},
{
prop: "fault_description_id",
label: "故障描述",
component: "ApiSelect",
componentProps: {
api: ProduceRepairRecordService.faultDescriptionOptions,
labelField: "description",
valueField: "id",
clearable: true,
},
colSpan: 8,
},
],
},
columns: [
{ type: "selection" },
{ type: "index", label: "序号", width: 70 },
{ prop: "repair_no", label: "维修单号", minWidth: 160 },
{
prop: "order_no",
label: "订单号/商品编码",
minWidth: 180,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { order_no?: string | null; product_code?: string | null }) =>
[row.order_no, row.product_code].filter(Boolean).join("/"),
},
{ prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } },
{
prop: "part_category",
label: "物料分类",
minWidth: 140,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.category_name || ""),
},
{
prop: "part_code",
label: "物料编码",
minWidth: 150,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.product_code || ""),
},
{
prop: "part_name",
label: "更换备件",
minWidth: 240,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => part.product_name || "", true),
},
{
prop: "part_fault",
label: "故障描述",
minWidth: 180,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, formatPartFaultDescriptions),
},
{
prop: "part_quantity",
label: "数量",
width: 80,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => String(part.quantity ?? 1)),
},
{
prop: "part_source",
label: "来源",
width: 90,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { parts?: Array<Record<string, any>> | string | null }) =>
formatPartRows(row.parts, (part) => formatPartSource(part.source)),
},
{
prop: "repair_result",
label: "维修判定分类",
minWidth: 140,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { repair_result?: string | null }) => formatRepairResult(row.repair_result),
},
{
prop: "responsible_operators",
label: "责任人员",
minWidth: 120,
cellStyle: { whiteSpace: "nowrap" },
formatter: (row: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) =>
formatResponsibleOperatorNames(row.responsible_operators),
},
{ prop: "operator", label: "维修人员", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "repaired_date", label: "维修时间", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } },
],
dialogProps: { width: "860px" },
formSchema: {
showDefaultActions: false,
labelWidth: "105px",
schema: [
{
component: "Input",
prop: "id",
label: "ID",
show: false,
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "repair_no",
label: "维修单号",
componentProps: { maxlength: 100, placeholder: "请输入维修单号" },
rules: [{ required: true, message: "请输入维修单号" }],
colSpan: 12,
},
{
component: "Input",
prop: "order_no",
label: "订单号",
componentProps: { maxlength: 100, placeholder: "请输入订单号" },
colSpan: 12,
},
{
component: "Input",
prop: "product_code",
label: "商品编码",
componentProps: { maxlength: 100, placeholder: "请输入商品编码" },
colSpan: 12,
},
{
component: "Input",
prop: "product_name",
label: "商品名称",
componentProps: { maxlength: 200, placeholder: "请输入商品名称" },
colSpan: 12,
},
{
component: "ApiSelect",
prop: "category_id",
label: "首选物料分类",
componentProps: {
api: ProduceRepairRecordService.categoryOptions,
labelField: "label",
valueField: "value",
clearable: true,
placeholder: "请选择首选物料分类",
},
colSpan: 12,
},
{
component: "Input",
prop: "category_names",
label: "物料分类",
componentProps: (model: { category_names?: string[] | string | null; category_name?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatCategoryNames(model),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "ApiSelect",
prop: "fault_description_id",
label: "故障描述",
componentProps: {
api: ProduceRepairRecordService.faultDescriptionOptions,
labelField: "description",
valueField: "id",
clearable: true,
placeholder: "请选择硬件故障描述",
},
colSpan: 12,
},
{
component: "Input",
prop: "exception_reason",
label: "异常原因",
componentProps: (model: { exception_reason?: string | null; exception_issue?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: [model.exception_reason, model.exception_issue].filter(Boolean).join(" · "),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "fault_descriptions",
label: "故障描述",
componentProps: (model: { fault_descriptions?: string[]; fault_description?: string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: (model.fault_descriptions || []).join("、") || model.fault_description || "",
}),
ifDetail: true,
colSpan: 24,
},
{
component: "PartsDetail",
prop: "parts",
label: "更换备件",
ifDetail: true,
colSpan: 24,
},
{
component: "Select",
prop: "repair_result",
label: "维修判定",
componentProps: {
clearable: true,
options: [
{ label: "物料不良", value: "material_defect" },
{ label: "作业不良", value: "operation_defect" },
{ label: "搭配错误", value: "assembly_mismatch" },
],
},
colSpan: 12,
},
{
component: "Input",
prop: "repair_result_detail",
label: "维修判定",
componentProps: (model: { repair_result?: string | null }) => ({
readonly: true,
value: formatRepairResult(model.repair_result),
}),
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "operation_fault_steps",
label: "责任步骤",
componentProps: (model: { operation_fault_steps?: ResponsibilitySnapshot[] | string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatOperationFaultSteps(model.operation_fault_steps),
}),
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "responsible_operators",
label: "责任人员",
componentProps: (model: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => ({
type: "textarea",
rows: 2,
readonly: true,
value: formatResponsibleOperators(model.responsible_operators),
}),
ifDetail: true,
colSpan: 24,
},
{
component: "Upload",
prop: "images",
label: "维修现场图片",
componentProps: { listType: "picture-card" },
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "operator",
label: "维修人员",
componentProps: { maxlength: 100, placeholder: "请输入维修人员" },
colSpan: 12,
},
{
component: "DatePicker",
prop: "repaired_at",
label: "维修时间",
componentProps: {
type: "datetime",
valueFormat: "x",
placeholder: "请选择维修时间",
},
colSpan: 12,
},
{
component: "Input",
prop: "remark",
label: "备注",
componentProps: { type: "textarea", rows: 3, maxlength: 1000 },
colSpan: 24,
},
],
},
});
@@ -0,0 +1,22 @@
<template>
<div class="repair-setting-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceRepairSettingList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.repair-setting-page {
height: 100%;
}
</style>
@@ -0,0 +1,100 @@
import { CrudSchema } from '@/components/crud'
import { ProduceRepairSettingService } from '@/apps/produce/api'
const settingTypeOptions = [
{ label: '处理类型', value: 'type' },
{ label: '维修原因', value: 'reason' },
{ label: '具体问题', value: 'issue' },
]
export const crudSchema = (): CrudSchema => ({
api: ProduceRepairSettingService.list,
dialogTitle: '维修设置',
crudApi: {
add: ProduceRepairSettingService.create,
edit: ProduceRepairSettingService.update,
remove: ProduceRepairSettingService.remove,
view: ProduceRepairSettingService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:repair-setting:create',
editAuth: 'produce:repair-setting:update',
removeAuth: 'produce:repair-setting:delete',
viewAuth: 'produce:repair-setting:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '90px',
schema: [
{
prop: 'keyword', label: '关键字', component: 'Input',
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
},
{
prop: 'setting_type', label: '设置层级', component: 'Select',
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
},
{
prop: 'is_enabled', label: '启用状态', component: 'Select',
componentProps: {
clearable: true, placeholder: '请选择启用状态',
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
}, colSpan: 8,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
{ prop: 'code', label: '编码', minWidth: 150 },
{ prop: 'name', label: '名称', minWidth: 160 },
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
{ prop: 'sort', label: '排序', width: 90 },
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ prop: 'created_name', label: '创建人', minWidth: 110 },
],
dialogProps: { width: '700px' },
formSchema: {
showDefaultActions: false,
labelWidth: '105px',
schema: [
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
{
component: 'Select', prop: 'setting_type', label: '设置层级',
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
},
{
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
componentProps: {
api: '/produce/repair-setting', requestMethod: 'get',
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
}, colSpan: 12,
},
{
component: 'Input', prop: 'code', label: '设置编码',
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
},
{
component: 'Input', prop: 'name', label: '设置名称',
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
},
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
{
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
},
{
component: 'Input', prop: 'remark', label: '备注',
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
},
],
},
})
@@ -0,0 +1,22 @@
<template>
<div class="produce-scan-data-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceScanDataList' })
const [BasicCrud] = useCrud({
...crudSchema(),
})
</script>
<style scoped>
.produce-scan-data-page {
height: 100%;
}
</style>
@@ -0,0 +1,254 @@
import { CrudSchema } from '@/components/crud'
import { ProduceScanDataService } from '@/apps/produce/api'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceScanDataService.list,
dialogTitle: '扫码数据',
crudApi: {
add: ProduceScanDataService.create,
edit: ProduceScanDataService.update,
remove: ProduceScanDataService.remove,
view: ProduceScanDataService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:scan-data:create',
editAuth: 'produce:scan-data:update',
removeAuth: 'produce:scan-data:delete',
viewAuth: 'produce:scan-data:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'keyword',
label: '关键字',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入订单号/用户编码/操作人员/备注',
},
colSpan: 6,
},
{
prop: 'process_id',
label: '工序名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
},
colSpan: 6,
},
{
prop: 'step_id',
label: '步骤名称',
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
},
colSpan: 6,
},
{
prop: 'status_id',
label: '步骤状态',
component: 'ApiSelect',
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
},
colSpan: 6,
},
{
prop: 'LIKE_operator',
label: '操作人员',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入操作人员',
},
colSpan: 6,
},
{
prop: 'scan_result',
label: '扫码结果',
component: 'Select',
componentProps: {
clearable: true,
placeholder: '请选择扫码结果',
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
colSpan: 6,
},
{
prop: 'scan_time_range',
label: '扫码时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
clearable: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
colSpan: 12,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: '序号', width: 70 },
{ prop: 'order_no', label: '订单号', minWidth: 160 },
{ prop: 'quantity', label: '数量', width: 90 },
{ prop: 'user_code', label: '用户编码', minWidth: 120 },
{ prop: 'process_name', label: '工序名称', minWidth: 140 },
{ prop: 'step_name', label: '步骤名称', minWidth: 140 },
{ prop: 'status_name', label: '步骤状态', minWidth: 120 },
{ prop: 'operator', label: '操作人员', minWidth: 120 },
{
prop: 'scan_result',
label: '扫码结果',
width: 100,
formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'),
},
{ prop: 'scan_date', label: '扫码时间', minWidth: 180 },
{ prop: 'created_name', label: '创建人', minWidth: 120 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'order_no',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
maxlength: 100,
},
rules: [{ required: true, message: '请输入订单号' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'user_code',
label: '用户编码',
componentProps: {
placeholder: '请输入用户编码',
maxlength: 50,
},
rules: [{ required: true, message: '请输入用户编码' }],
colSpan: 12,
},
{
component: 'Input',
prop: 'operator',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员',
maxlength: 100,
},
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'process_id',
label: '工序名称',
componentProps: (values: any) => ({
api: '/produce/process',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择工序',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择工序' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'step_id',
label: '步骤名称',
componentProps: (values: any) => ({
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择步骤',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择步骤' }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'status_id',
label: '步骤状态',
componentProps: (values: any) => ({
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: '请选择状态',
disabled: values?.id !== undefined && values?.id !== null && values?.id !== '',
}),
rules: [{ required: true, message: '请选择状态' }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'scan_result',
label: '扫码结果',
defaultValue: 1,
componentProps: {
options: [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 },
],
},
rules: [{ required: true, message: '请选择扫码结果' }],
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: '备注',
componentProps: {
type: 'textarea',
rows: 3,
placeholder: '请输入备注',
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,310 @@
<template>
<div class="statistics-page art-full-height">
<div class="toolbar">
<el-date-picker
v-model="date"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
@change="loadData"
/>
<el-select v-model="filters.data_type" placeholder="数据类型" @change="loadData">
<el-option label="全部数据" value="all" />
<el-option label="订单数据" value="order" />
<el-option label="整机数据" value="machine" />
</el-select>
<el-input
v-model="filters.user_code"
clearable
placeholder="用户编码"
@keyup.enter="loadData"
/>
<el-input
v-model="filters.operator"
clearable
placeholder="操作人员"
@keyup.enter="loadData"
/>
<el-select
v-model="filters.process_id"
clearable
filterable
placeholder="选择工序名称"
@change="handleProcessChange"
>
<el-option
v-for="option in processOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-select
v-model="filters.step_id"
clearable
filterable
placeholder="选择步骤名称"
>
<el-option
v-for="option in stepOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-button type="primary" :loading="loading" @click="loadData"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
<el-button :disabled="!items.length" @click="exportData">导出</el-button>
</div>
<div class="table-wrapper" v-loading="loading">
<div class="table-scroll">
<el-table
:data="items"
border
stripe
:style="{ width: tableWidth }"
empty-text="暂无统计数据"
:span-method="spanMethod"
:row-class-name="rowClassName"
>
<el-table-column type="index" label="序号" width="65" fixed="left" />
<el-table-column
prop="user_code"
label="工位号"
width="110"
fixed="left"
/>
<el-table-column
prop="operator"
label="操作人员"
width="120"
fixed="left"
/>
<el-table-column prop="production_process" label="生产工序" width="220" fixed="left" />
<el-table-column prop="total" label="总数" width="75" fixed="left" />
<el-table-column
v-for="column in columns"
:key="column.key"
:prop="column.key"
:label="column.label"
width="64"
align="center"
/>
</el-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import {
ProduceWorkStatisticsService,
type WorkStatisticsColumn,
type WorkStatisticsOption,
} from "@/apps/produce/api/statistics";
import { downloadFile } from "@/utils/tool";
interface StatisticsRow {
user_code?: string;
operator?: string;
production_process?: string;
process_name?: string;
step_name?: string;
total?: number;
[key: string]: string | number | null | undefined;
}
defineOptions({ name: "ProduceDailyWorkStatistics" });
const date = ref(new Date().toISOString().slice(0, 10));
const filters = reactive({
data_type: "all",
user_code: "",
operator: "",
process_id: undefined as string | number | undefined,
step_id: undefined as string | number | undefined,
});
const columns = ref<WorkStatisticsColumn[]>([]);
const items = ref<StatisticsRow[]>([]);
const processOptions = ref<WorkStatisticsOption[]>([]);
const stepOptions = ref<WorkStatisticsOption[]>([]);
const loading = ref(false);
const tableWidth = computed(() => `${590 + columns.value.length * 64}px`);
function spanMethod({
rowIndex,
columnIndex,
}: {
rowIndex: number;
columnIndex: number;
}) {
if (columnIndex !== 1 && columnIndex !== 2) {
return [1, 1];
}
const current = items.value[rowIndex];
if (current.user_code === "总计") {
return [1, 1];
}
const previous = items.value[rowIndex - 1];
if (
previous &&
previous.user_code !== "总计" &&
current.user_code === previous.user_code &&
current.operator === previous.operator
) {
return [0, 0];
}
let rowspan = 1;
for (let index = rowIndex + 1; index < items.value.length; index++) {
const next = items.value[index];
if (
next.user_code === "总计" ||
next.user_code !== current.user_code ||
next.operator !== current.operator
) {
break;
}
rowspan++;
}
return [rowspan, 1];
}
function rowClassName({ row }: { row: StatisticsRow }) {
return row.user_code === "总计" ? "summary-row" : "";
}
function exportData() {
const fields = [
{ key: "user_code", label: "工位号" },
{ key: "operator", label: "操作人员" },
{ key: "production_process", label: "生产工序" },
{ key: "total", label: "总数" },
...columns.value,
];
const escapeCsv = (value: unknown) =>
`"${String(value ?? "").replaceAll('"', '""')}"`;
const csv = [
fields.map((field) => escapeCsv(field.label)).join(","),
...items.value.map((item) =>
fields.map((field) => escapeCsv(item[field.key])).join(","),
),
].join("\r\n");
downloadFile(
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }),
`日工作统计_${date.value}.csv`,
);
}
async function loadData() {
loading.value = true;
try {
const response = await ProduceWorkStatisticsService.daily({
date: date.value,
...filters,
});
columns.value = response?.columns || [];
items.value = (response?.items || []) as StatisticsRow[];
} finally {
loading.value = false;
}
}
async function resetFilters() {
date.value = new Date().toISOString().slice(0, 10);
filters.data_type = "all";
filters.user_code = "";
filters.operator = "";
filters.process_id = undefined;
filters.step_id = undefined;
await loadStepOptions();
await loadData();
}
async function loadStepOptions() {
stepOptions.value = await ProduceWorkStatisticsService.getStepOptions(
filters.process_id,
);
}
async function handleProcessChange() {
filters.step_id = undefined;
await loadStepOptions();
}
onMounted(async () => {
const [processes] = await Promise.all([
ProduceWorkStatisticsService.getProcessOptions(),
loadStepOptions(),
]);
processOptions.value = processes;
await loadData();
});
</script>
<style scoped>
.statistics-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.toolbar .el-input,
.toolbar .el-select {
width: 180px;
}
.table-wrapper {
flex: 1;
min-height: 0;
margin-top: 12px;
background: var(--el-bg-color);
overflow: hidden;
}
.table-scroll {
width: 100%;
height: 100%;
overflow: auto;
}
:deep(.el-table__header-wrapper th .cell) {
white-space: nowrap;
}
:deep(.el-table .cell) {
overflow: visible;
text-overflow: clip;
white-space: nowrap;
}
:deep(.el-table .summary-row) {
--el-table-tr-bg-color: #fff7d6;
}
:deep(.el-table .summary-row > td.el-table__cell) {
background-color: #fff7d6 !important;
}
:deep(.el-table .summary-row:hover > td.el-table__cell) {
background-color: #fff2b8 !important;
}
</style>
@@ -0,0 +1,290 @@
<template>
<div class="statistics-page art-full-height">
<div class="toolbar">
<el-date-picker
v-model="month"
type="month"
value-format="YYYY-MM"
placeholder="选择月份"
@change="loadData"
/>
<el-select v-model="filters.data_type" placeholder="数据类型" @change="loadData">
<el-option label="全部数据" value="all" />
<el-option label="订单数据" value="order" />
<el-option label="整机数据" value="machine" />
</el-select>
<el-input
v-model="filters.user_code"
clearable
placeholder="用户编码"
@keyup.enter="loadData"
/>
<el-input
v-model="filters.operator"
clearable
placeholder="操作人员"
@keyup.enter="loadData"
/>
<el-select
v-model="filters.process_id"
clearable
filterable
placeholder="选择工序名称"
@change="handleProcessChange"
>
<el-option
v-for="option in processOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-select
v-model="filters.step_id"
clearable
filterable
placeholder="选择步骤名称"
>
<el-option
v-for="option in stepOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-button type="primary" :loading="loading" @click="loadData"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
<el-button :disabled="!items.length" @click="exportData">导出</el-button>
</div>
<div class="table-wrapper" v-loading="loading">
<el-table
:data="items"
border
stripe
height="100%"
empty-text="暂无统计数据"
:span-method="spanMethod"
:row-class-name="rowClassName"
>
<el-table-column type="index" label="序号" width="65" fixed="left" />
<el-table-column
prop="user_code"
label="工位号"
width="110"
fixed="left"
/>
<el-table-column
prop="operator"
label="操作人员"
width="120"
fixed="left"
/>
<el-table-column prop="production_process" label="生产工序" width="220" fixed="left" />
<el-table-column prop="total" label="总数" width="75" fixed="left" />
<el-table-column
v-for="column in columns"
:key="column.key"
:prop="column.key"
:label="column.label"
width="58"
align="center"
/>
</el-table>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import {
ProduceWorkStatisticsService,
type WorkStatisticsColumn,
type WorkStatisticsOption,
} from "@/apps/produce/api/statistics";
import { downloadFile } from "@/utils/tool";
interface StatisticsRow {
user_code?: string;
operator?: string;
production_process?: string;
process_name?: string;
step_name?: string;
total?: number;
[key: string]: string | number | null | undefined;
}
defineOptions({ name: "ProduceMonthlyWorkStatistics" });
const month = ref(new Date().toISOString().slice(0, 7));
const filters = reactive({
data_type: "all",
user_code: "",
operator: "",
process_id: undefined as string | number | undefined,
step_id: undefined as string | number | undefined,
});
const columns = ref<WorkStatisticsColumn[]>([]);
const items = ref<StatisticsRow[]>([]);
const processOptions = ref<WorkStatisticsOption[]>([]);
const stepOptions = ref<WorkStatisticsOption[]>([]);
const loading = ref(false);
function spanMethod({
rowIndex,
columnIndex,
}: {
rowIndex: number;
columnIndex: number;
}) {
if (columnIndex !== 1 && columnIndex !== 2) {
return [1, 1];
}
const current = items.value[rowIndex];
if (current.user_code === "总计") {
return [1, 1];
}
const previous = items.value[rowIndex - 1];
if (
previous &&
previous.user_code !== "总计" &&
current.user_code === previous.user_code &&
current.operator === previous.operator
) {
return [0, 0];
}
let rowspan = 1;
for (let index = rowIndex + 1; index < items.value.length; index++) {
const next = items.value[index];
if (
next.user_code === "总计" ||
next.user_code !== current.user_code ||
next.operator !== current.operator
) {
break;
}
rowspan++;
}
return [rowspan, 1];
}
function rowClassName({ row }: { row: StatisticsRow }) {
return row.user_code === "总计" ? "summary-row" : "";
}
function exportData() {
const fields = [
{ key: "user_code", label: "工位号" },
{ key: "operator", label: "操作人员" },
{ key: "production_process", label: "生产工序" },
{ key: "total", label: "总数" },
...columns.value,
];
const escapeCsv = (value: unknown) =>
`"${String(value ?? "").replaceAll('"', '""')}"`;
const csv = [
fields.map((field) => escapeCsv(field.label)).join(","),
...items.value.map((item) =>
fields.map((field) => escapeCsv(item[field.key])).join(","),
),
].join("\r\n");
downloadFile(
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }),
`月工作统计_${month.value}.csv`,
);
}
async function loadData() {
loading.value = true;
try {
const response = await ProduceWorkStatisticsService.monthly({
month: month.value,
...filters,
});
columns.value = response?.columns || [];
items.value = (response?.items || []) as StatisticsRow[];
} finally {
loading.value = false;
}
}
async function resetFilters() {
month.value = new Date().toISOString().slice(0, 7);
filters.data_type = "all";
filters.user_code = "";
filters.operator = "";
filters.process_id = undefined;
filters.step_id = undefined;
await loadStepOptions();
await loadData();
}
async function loadStepOptions() {
stepOptions.value = await ProduceWorkStatisticsService.getStepOptions(
filters.process_id,
);
}
async function handleProcessChange() {
filters.step_id = undefined;
await loadStepOptions();
}
onMounted(async () => {
const [processes] = await Promise.all([
ProduceWorkStatisticsService.getProcessOptions(),
loadStepOptions(),
]);
processOptions.value = processes;
await loadData();
});
</script>
<style scoped>
.statistics-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
background: var(--el-bg-color-page);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
}
.toolbar .el-input,
.toolbar .el-select {
width: 180px;
}
:deep(.el-table .summary-row) {
--el-table-tr-bg-color: #fff7d6;
}
:deep(.el-table .summary-row > td.el-table__cell) {
background-color: #fff7d6 !important;
}
:deep(.el-table .summary-row:hover > td.el-table__cell) {
background-color: #fff2b8 !important;
}
.table-wrapper {
flex: 1;
min-height: 0;
margin-top: 12px;
background: var(--el-bg-color);
}
</style>
@@ -0,0 +1,26 @@
<template>
<div class="produce-status-transition-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStatusTransitionList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-status-transition-page {
height: 100%;
}
</style>
@@ -0,0 +1,290 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStatusTransitionService } from '../../../../api/index'
const triggerEventOptions = [
{ label: '下一工序扫码触发', value: 'next_step_scan' },
{ label: '扫描触发', value: 'scan_trigger' },
{ label: '异常触发', value: 'exception_trigger' },
{ label: '手动触发', value: 'manual_trigger' },
{ label: '定时触发', value: 'timer_trigger' },
{ label: '完成触发', value: 'complete_trigger' },
]
const transitionTypeOptions = [
{ label: '主转换', value: 'primary' },
{ label: '副作用', value: 'side_effect' },
]
const sideEffectTypeOptions = [
{ label: '记录日志', value: 'log' },
{ label: '发送通知', value: 'notify' },
{ label: '更新字段', value: 'update_field' },
{ label: '触发事件', value: 'trigger_event' },
]
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStatusTransitionService.list,
dialogTitle: $t('produce.modules.status_transition.title'),
crudApi: {
add: ProduceStatusTransitionService.create,
edit: ProduceStatusTransitionService.update,
remove: ProduceStatusTransitionService.remove,
view: ProduceStatusTransitionService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:status-transition:create',
editAuth: 'produce:status-transition:update',
removeAuth: 'produce:status-transition:delete',
viewAuth: 'produce:status-transition:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'step_id',
label: $t('produce.modules.status_transition.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'trigger_event',
label: $t('produce.modules.status_transition.search.trigger_event'),
component: 'Select',
componentProps: {
options: triggerEventOptions,
clearable: true,
placeholder: $t('produce.modules.status_transition.search.trigger_event_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.status_transition.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.status_transition.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'step.step_name', label: $t('produce.modules.status_transition.columns.step_name'), minWidth: 160 },
{ prop: 'from_status.status_name', label: $t('produce.modules.status_transition.columns.from_status_name'), minWidth: 140 },
{ prop: 'to_status.status_name', label: $t('produce.modules.status_transition.columns.to_status_name'), minWidth: 140 },
{ prop: 'trigger_event', label: $t('produce.modules.status_transition.columns.trigger_event'), minWidth: 160 },
{ prop: 'transition_type', label: $t('produce.modules.status_transition.columns.transition_type'), width: 120 },
{
prop: 'auto_trigger',
label: $t('produce.modules.status_transition.columns.auto_trigger'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'priority', label: $t('produce.modules.status_transition.columns.priority'), width: 100 },
{ prop: 'sort', label: $t('produce.modules.status_transition.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.status_transition.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.status_transition.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '860px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'step_id',
label: $t('produce.modules.status_transition.form.step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.step_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'from_status_id',
label: $t('produce.modules.status_transition.form.from_status_id'),
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.from_status_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.from_status_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'to_status_id',
label: $t('produce.modules.status_transition.form.to_status_id'),
componentProps: {
api: '/produce/step-status',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.status_transition.form.to_status_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.to_status_id_placeholder') }],
colSpan: 12,
},
{
component: 'Select',
prop: 'trigger_event',
label: $t('produce.modules.status_transition.columns.trigger_event'),
componentProps: {
options: triggerEventOptions,
placeholder: $t('produce.modules.status_transition.form.trigger_event_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.status_transition.form.trigger_event_placeholder') }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'transition_type',
label: $t('produce.modules.status_transition.columns.transition_type'),
defaultValue: 'primary',
componentProps: {
options: transitionTypeOptions,
isBtn: true,
renderType: 'RadioButton',
},
colSpan: 24,
},
{
component: 'Select',
prop: 'side_effect_type',
label: $t('produce.modules.status_transition.form.side_effect_type'),
componentProps: {
options: sideEffectTypeOptions,
clearable: true,
placeholder: $t('produce.modules.status_transition.form.side_effect_type_placeholder'),
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'auto_trigger',
label: $t('produce.modules.status_transition.columns.auto_trigger'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'priority',
label: $t('produce.modules.status_transition.columns.priority'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.status_transition.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'event_config',
label: $t('produce.modules.status_transition.form.event_config'),
componentProps: {
type: 'textarea',
rows: 5,
placeholder: $t('produce.modules.status_transition.form.event_config_placeholder'),
},
colSpan: 24,
},
{
component: 'Input',
prop: 'side_effect_config',
label: $t('produce.modules.status_transition.form.side_effect_config'),
componentProps: {
type: 'textarea',
rows: 5,
placeholder: $t('produce.modules.status_transition.form.side_effect_config_placeholder'),
},
colSpan: 24,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.status_transition.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.status_transition.form.description'),
componentProps: {
type: 'textarea',
rows: 3,
placeholder: $t('produce.modules.status_transition.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-step-relation-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStepRelationList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-step-relation-page {
height: 100%;
}
</style>
@@ -0,0 +1,218 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStepRelationService } from '../../../../api/index'
const relationTypeOptions = [
{ label: '正常流转', value: 'normal' },
{ label: '异常流转', value: 'exception' },
]
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStepRelationService.list,
dialogTitle: $t('produce.modules.step_relation.title'),
crudApi: {
add: ProduceStepRelationService.create,
edit: ProduceStepRelationService.update,
remove: ProduceStepRelationService.remove,
view: ProduceStepRelationService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:step-relation:create',
editAuth: 'produce:step-relation:update',
removeAuth: 'produce:step-relation:delete',
viewAuth: 'produce:step-relation:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'from_step_id',
label: $t('produce.modules.step_relation.search.from_step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.search.from_step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'to_step_id',
label: $t('produce.modules.step_relation.search.to_step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.search.to_step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'relation_type',
label: $t('produce.modules.step_relation.search.relation_type'),
component: 'Select',
componentProps: {
options: relationTypeOptions,
clearable: true,
placeholder: $t('produce.modules.step_relation.search.relation_type_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.step_relation.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.step_relation.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'from_step.step_name', label: $t('produce.modules.step_relation.columns.from_step_name'), minWidth: 180 },
{ prop: 'to_step.step_name', label: $t('produce.modules.step_relation.columns.to_step_name'), minWidth: 180 },
{ prop: 'relation_type', label: $t('produce.modules.step_relation.columns.relation_type'), width: 120 },
{ prop: 'priority', label: $t('produce.modules.step_relation.columns.priority'), width: 100 },
{ prop: 'flow_condition', label: $t('produce.modules.step_relation.columns.flow_condition'), minWidth: 220 },
{ prop: 'sort', label: $t('produce.modules.step_relation.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.step_relation.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.step_relation.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '760px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'from_step_id',
label: $t('produce.modules.step_relation.form.from_step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.form.from_step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_relation.form.from_step_id_placeholder') }],
colSpan: 12,
},
{
component: 'ApiSelect',
prop: 'to_step_id',
label: $t('produce.modules.step_relation.form.to_step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_relation.form.to_step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_relation.form.to_step_id_placeholder') }],
colSpan: 12,
},
{
component: 'RadioGroup',
prop: 'relation_type',
label: $t('produce.modules.step_relation.columns.relation_type'),
defaultValue: 'normal',
componentProps: {
options: relationTypeOptions,
isBtn: true,
renderType: 'RadioButton',
},
colSpan: 24,
},
{
component: 'InputNumber',
prop: 'priority',
label: $t('produce.modules.step_relation.columns.priority'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.step_relation.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'Input',
prop: 'flow_condition',
label: $t('produce.modules.step_relation.form.flow_condition'),
componentProps: {
type: 'textarea',
rows: 6,
placeholder: $t('produce.modules.step_relation.form.flow_condition_placeholder'),
},
colSpan: 24,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.step_relation.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.step_relation.form.description'),
componentProps: {
type: 'textarea',
rows: 3,
placeholder: $t('produce.modules.step_relation.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-step-status-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
defineOptions({ name: 'ProduceStepStatusList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-step-status-page {
height: 100%;
}
</style>
@@ -0,0 +1,227 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceStepStatusService } from '../../../../api/index'
export const crudSchema = (): CrudSchema => {
return {
api: ProduceStepStatusService.list,
dialogTitle: $t('produce.modules.step_status.title'),
crudApi: {
add: ProduceStepStatusService.create,
edit: ProduceStepStatusService.update,
remove: ProduceStepStatusService.remove,
view: ProduceStepStatusService.get,
},
useCrud: true,
hasAdd: true,
hasEdit: true,
hasRemove: true,
hasView: true,
addAuth: 'produce:step-status:create',
editAuth: 'produce:step-status:update',
removeAuth: 'produce:step-status:delete',
viewAuth: 'produce:step-status:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'step_id',
label: $t('produce.modules.step_status.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_status.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_status_code',
label: $t('produce.modules.step_status.search.status_code'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_code_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_status_name',
label: $t('produce.modules.step_status.search.status_name'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_name_placeholder'),
},
colSpan: 6,
},
{
prop: 'status',
label: $t('produce.modules.step_status.search.status'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.step_status.search.status_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'selection' },
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{
prop: 'step.step_name',
label: $t('produce.modules.step_status.columns.step_name'),
minWidth: 180,
},
{ prop: 'status_code', label: $t('produce.modules.step_status.columns.status_code'), minWidth: 160 },
{ prop: 'status_name', label: $t('produce.modules.step_status.columns.status_name'), minWidth: 180 },
{
prop: 'is_initial',
label: $t('produce.modules.step_status.columns.is_initial'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{
prop: 'is_final',
label: $t('produce.modules.step_status.columns.is_final'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'sort', label: $t('produce.modules.step_status.columns.sort'), width: 100 },
{
prop: 'status',
label: $t('produce.modules.step_status.columns.status'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'created_date', label: $t('produce.modules.step_status.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '720px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '110px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 24,
prop: 'id',
label: 'ID',
},
{
component: 'ApiSelect',
prop: 'step_id',
label: $t('produce.modules.step_status.form.step_id'),
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.step_status.form.step_id_placeholder'),
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.step_id_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'status_code',
label: $t('produce.modules.step_status.columns.status_code'),
componentProps: {
placeholder: $t('produce.modules.step_status.form.status_code_placeholder'),
maxlength: 50,
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.status_code_placeholder') }],
colSpan: 12,
},
{
component: 'Input',
prop: 'status_name',
label: $t('produce.modules.step_status.columns.status_name'),
componentProps: {
placeholder: $t('produce.modules.step_status.form.status_name_placeholder'),
maxlength: 100,
},
rules: [{ required: true, message: $t('produce.modules.step_status.form.status_name_placeholder') }],
colSpan: 12,
},
{
component: 'InputNumber',
prop: 'sort',
label: $t('produce.modules.step_status.columns.sort'),
defaultValue: 0,
componentProps: {
min: 0,
class: 'w-full',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_initial',
label: $t('produce.modules.step_status.columns.is_initial'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_final',
label: $t('produce.modules.step_status.columns.is_final'),
defaultValue: 0,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'status',
label: $t('produce.modules.step_status.columns.status'),
defaultValue: 1,
componentProps: {
code: DictEnum.SYS_YES_NO,
isBtn: true,
renderType: 'RadioGroup',
},
colSpan: 24,
},
{
component: 'Input',
prop: 'description',
label: $t('produce.modules.step_status.form.description'),
componentProps: {
type: 'textarea',
rows: 4,
placeholder: $t('produce.modules.step_status.form.description_placeholder'),
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,26 @@
<template>
<div class="produce-transition-log-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas/index'
defineOptions({ name: 'ProduceTransitionLogList' })
const [BasicCrud] = useCrud({
...crudSchema(),
toolbarActions: [],
dropDownToolbarActions: [],
tableActions: [],
dropDownActions: [],
})
</script>
<style scoped>
.produce-transition-log-page {
height: 100%;
}
</style>
@@ -0,0 +1,277 @@
import { CrudSchema } from '@/components/crud'
import { DictEnum } from '@/enums'
import { $t } from '@/locales'
import { ProduceTransitionLogService } from '../../../../api/index'
const triggerEventOptions = [
{ label: '下一工序扫码触发', value: 'next_step_scan' },
{ label: '扫描触发', value: 'scan_trigger' },
{ label: '异常触发', value: 'exception_trigger' },
{ label: '手动触发', value: 'manual_trigger' },
{ label: '定时触发', value: 'timer_trigger' },
{ label: '完成触发', value: 'complete_trigger' },
]
const formatJsonText = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return ''
}
if (typeof value === 'string') {
return value
}
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
export const crudSchema = (): CrudSchema => {
return {
api: ProduceTransitionLogService.list,
dialogTitle: $t('produce.modules.transition_log.title'),
crudApi: {
view: async (id: string | number) => {
const res = await ProduceTransitionLogService.get(id)
return {
...res,
trigger_data: formatJsonText(res?.trigger_data),
}
},
},
useCrud: true,
hasAdd: false,
hasEdit: false,
hasRemove: false,
hasView: true,
viewAuth: 'produce:transition-log:read',
rowKey: 'id',
searchFormSchema: {
labelWidth: '100px',
schema: [
{
prop: 'LIKE_work_order_no',
label: $t('produce.modules.transition_log.search.work_order_no'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.transition_log.search.work_order_no_placeholder'),
},
colSpan: 6,
},
{
prop: 'step_id',
label: $t('produce.modules.transition_log.search.step_id'),
component: 'ApiSelect',
componentProps: {
api: '/produce/process-step',
requestMethod: 'get',
params: { format: 'select' },
clearable: true,
placeholder: $t('produce.modules.transition_log.search.step_id_placeholder'),
},
colSpan: 6,
},
{
prop: 'trigger_event',
label: $t('produce.modules.transition_log.search.trigger_event'),
component: 'Select',
componentProps: {
options: triggerEventOptions,
clearable: true,
placeholder: $t('produce.modules.transition_log.search.trigger_event_placeholder'),
},
colSpan: 6,
},
{
prop: 'LIKE_trigger_user',
label: $t('produce.modules.transition_log.search.trigger_user'),
component: 'Input',
componentProps: {
clearable: true,
placeholder: $t('produce.modules.transition_log.search.trigger_user_placeholder'),
},
colSpan: 6,
},
{
prop: 'is_success',
label: $t('produce.modules.transition_log.search.is_success'),
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
clearable: true,
placeholder: $t('produce.modules.transition_log.search.is_success_placeholder'),
},
colSpan: 6,
},
],
},
columns: [
{ type: 'index', label: $t('produce.common.columns.index'), width: 70 },
{ prop: 'work_order_no', label: $t('produce.modules.transition_log.columns.work_order_no'), minWidth: 140 },
{ prop: 'step.step_name', label: $t('produce.modules.transition_log.columns.step_name'), minWidth: 160 },
{ prop: 'from_status.status_name', label: $t('produce.modules.transition_log.columns.from_status_name'), minWidth: 140 },
{ prop: 'to_status.status_name', label: $t('produce.modules.transition_log.columns.to_status_name'), minWidth: 140 },
{
prop: 'trigger_event',
label: $t('produce.modules.transition_log.columns.trigger_event'),
minWidth: 160,
formatter: (row: { trigger_event?: string }) =>
triggerEventOptions.find((option) => option.value === row.trigger_event)?.label || row.trigger_event || ''
},
{ prop: 'trigger_user', label: $t('produce.modules.transition_log.columns.trigger_user'), minWidth: 120 },
{
prop: 'is_auto',
label: $t('produce.modules.transition_log.columns.is_auto'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{
prop: 'is_success',
label: $t('produce.modules.transition_log.columns.is_success'),
width: 120,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_YES_NO,
},
},
{ prop: 'duration', label: $t('produce.modules.transition_log.columns.duration'), width: 120 },
{ prop: 'error_message', label: $t('produce.modules.transition_log.columns.error_message'), minWidth: 200 },
{ prop: 'created_date', label: $t('produce.modules.transition_log.columns.created_at'), minWidth: 180 },
],
dialogProps: {
width: '960px',
},
formSchema: {
showDefaultActions: false,
labelWidth: '120px',
schema: [
{
component: 'Input',
show: false,
ifDetail: true,
colSpan: 12,
prop: 'id',
label: 'ID',
},
{
component: 'Input',
prop: 'work_order_no',
label: $t('produce.modules.transition_log.columns.work_order_no'),
colSpan: 12,
},
{
component: 'Input',
prop: 'step.step_name',
label: $t('produce.modules.transition_log.columns.step_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'transition.trigger_event',
label: $t('produce.modules.transition_log.columns.transition_event'),
colSpan: 12,
},
{
component: 'Input',
prop: 'from_status.status_name',
label: $t('produce.modules.transition_log.columns.from_status_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'to_status.status_name',
label: $t('produce.modules.transition_log.columns.to_status_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_event',
label: $t('produce.modules.transition_log.columns.trigger_event'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_user',
label: $t('produce.modules.transition_log.columns.trigger_user'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_ip',
label: $t('produce.modules.transition_log.columns.trigger_ip'),
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_auto',
label: $t('produce.modules.transition_log.columns.is_auto'),
componentProps: {
code: DictEnum.SYS_YES_NO,
},
colSpan: 12,
},
{
component: 'ApiDict',
prop: 'is_success',
label: $t('produce.modules.transition_log.columns.is_success'),
componentProps: {
code: DictEnum.SYS_YES_NO,
},
colSpan: 12,
},
{
component: 'Input',
prop: 'duration',
label: $t('produce.modules.transition_log.columns.duration'),
colSpan: 12,
},
{
component: 'Input',
prop: 'created_name',
label: $t('produce.modules.transition_log.columns.created_name'),
colSpan: 12,
},
{
component: 'Input',
prop: 'created_date',
label: $t('produce.modules.transition_log.columns.created_at'),
colSpan: 12,
},
{
component: 'Input',
prop: 'trigger_data',
label: $t('produce.modules.transition_log.columns.trigger_data'),
componentProps: {
type: 'textarea',
rows: 6,
},
colSpan: 24,
},
{
component: 'Input',
prop: 'error_message',
label: $t('produce.modules.transition_log.columns.error_message'),
componentProps: {
type: 'textarea',
rows: 4,
},
colSpan: 24,
},
{
component: 'Input',
prop: 'remark',
label: $t('produce.modules.transition_log.columns.remark'),
componentProps: {
type: 'textarea',
rows: 4,
},
colSpan: 24,
},
],
},
}
}
@@ -0,0 +1,18 @@
import request from '@/utils/http';
/**
* test01 插件 API
*/
export default {
/**
* 列表 API
* @param params 查询参数
* @returns 响应数据
*/
async test(params = {}) {
return request.get<{ msg?: string; data?: unknown }>({
url: '/test01/index',
params
});
}
};
@@ -0,0 +1,8 @@
{
"test01": {
"title": "插件测试0",
"description": "",
"card_title": "插件测试0 Data",
"refresh_btn": "Refresh"
}
}
@@ -0,0 +1,8 @@
{
"test01": {
"title": "插件测试0",
"description": "",
"card_title": "插件测试0数据",
"refresh_btn": "刷新"
}
}