feat: add article and production modules
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="produce-hardware-fault-description-page art-full-height">
|
||||
<div class="hardware-fault-actions">
|
||||
<ElUpload :show-file-list="false" accept=".xlsx,.xls,.csv" :http-request="importHardwareFaultDescriptions">
|
||||
<ElButton type="primary" plain>Excel 导入</ElButton>
|
||||
</ElUpload>
|
||||
<span class="hardware-fault-hint">表头:分类、描述;同分类同描述将自动跳过并计为更新。</span>
|
||||
</div>
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { ProduceHardwareFaultDescriptionService } from '@/apps/produce/api'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ProduceHardwareFaultDescriptionList' })
|
||||
|
||||
const [BasicCrud, crudExpose] = useCrud({
|
||||
...crudSchema(),
|
||||
toolbarActions: [],
|
||||
dropDownToolbarActions: [],
|
||||
tableActions: [],
|
||||
dropDownActions: [],
|
||||
})
|
||||
|
||||
async function importHardwareFaultDescriptions(options: UploadRequestOptions) {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', options.file)
|
||||
const result = await ProduceHardwareFaultDescriptionService.importExcel(formData)
|
||||
ElMessage.success(`导入成功:新增 ${result.created} 条,更新 ${result.updated} 条`)
|
||||
options.onSuccess?.(result)
|
||||
crudExpose.refreshCreate()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : 'Excel导入失败')
|
||||
options.onError?.(error as Error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.produce-hardware-fault-description-page { height: 100%; }
|
||||
.hardware-fault-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.hardware-fault-hint { color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
</style>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { CrudSchema } from "@/components/crud";
|
||||
import { ProduceHardwareFaultDescriptionService } from "@/apps/produce/api";
|
||||
|
||||
const enabledOptions = [
|
||||
{ label: "启用", value: 1 },
|
||||
{ label: "停用", value: 0 },
|
||||
];
|
||||
|
||||
export const crudSchema = (): CrudSchema => ({
|
||||
api: ProduceHardwareFaultDescriptionService.list,
|
||||
dialogTitle: "硬件故障描述",
|
||||
crudApi: {
|
||||
add: ProduceHardwareFaultDescriptionService.create,
|
||||
edit: ProduceHardwareFaultDescriptionService.update,
|
||||
remove: ProduceHardwareFaultDescriptionService.remove,
|
||||
view: ProduceHardwareFaultDescriptionService.get,
|
||||
},
|
||||
useCrud: true,
|
||||
hasAdd: true,
|
||||
hasEdit: true,
|
||||
hasRemove: true,
|
||||
hasView: true,
|
||||
addAuth: "produce:hardware-fault-description:create",
|
||||
editAuth: "produce:hardware-fault-description:update",
|
||||
removeAuth: "produce:hardware-fault-description:delete",
|
||||
viewAuth: "produce:hardware-fault-description:read",
|
||||
rowKey: "id",
|
||||
searchFormSchema: {
|
||||
labelWidth: "90px",
|
||||
schema: [
|
||||
{
|
||||
prop: "category_id",
|
||||
label: "物料分类",
|
||||
component: "ApiSelect",
|
||||
componentProps: {
|
||||
api: ProduceHardwareFaultDescriptionService.categoryOptions,
|
||||
labelField: "label",
|
||||
valueField: "value",
|
||||
clearable: true,
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
prop: "keyword",
|
||||
label: "关键字",
|
||||
component: "Input",
|
||||
componentProps: { clearable: true, placeholder: "故障描述或备注" },
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
prop: "is_enabled",
|
||||
label: "启用状态",
|
||||
component: "Select",
|
||||
componentProps: { clearable: true, options: enabledOptions },
|
||||
colSpan: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{ type: "selection" },
|
||||
{ type: "index", label: "序号", width: 70 },
|
||||
{ prop: "category_name", label: "物料分类", minWidth: 160 },
|
||||
{
|
||||
prop: "description",
|
||||
label: "硬件故障描述",
|
||||
minWidth: 300,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{ prop: "use_count", label: "使用次数", width: 100 },
|
||||
{
|
||||
prop: "is_enabled",
|
||||
label: "状态",
|
||||
width: 90,
|
||||
formatter: (row: { is_enabled?: number }) =>
|
||||
row.is_enabled === 0 ? "停用" : "启用",
|
||||
},
|
||||
{ prop: "remark", label: "备注", minWidth: 180, showOverflowTooltip: true },
|
||||
{ prop: "created_date", label: "创建时间", minWidth: 180 },
|
||||
],
|
||||
dialogProps: { width: "720px" },
|
||||
formSchema: {
|
||||
showDefaultActions: false,
|
||||
labelWidth: "110px",
|
||||
schema: [
|
||||
{
|
||||
component: "Input",
|
||||
prop: "id",
|
||||
label: "ID",
|
||||
show: false,
|
||||
ifDetail: true,
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: "ApiSelect",
|
||||
prop: "category_id",
|
||||
label: "物料分类",
|
||||
componentProps: {
|
||||
api: ProduceHardwareFaultDescriptionService.categoryOptions,
|
||||
labelField: "label",
|
||||
valueField: "value",
|
||||
clearable: true,
|
||||
placeholder: "请选择物料分类",
|
||||
},
|
||||
rules: [{ required: true, message: "请选择物料分类" }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: "RadioGroup",
|
||||
prop: "is_enabled",
|
||||
label: "是否启用",
|
||||
defaultValue: 1,
|
||||
componentProps: { options: enabledOptions },
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
prop: "description",
|
||||
label: "故障描述",
|
||||
componentProps: {
|
||||
maxlength: 500,
|
||||
placeholder: "请输入该物料分类的硬件故障描述",
|
||||
},
|
||||
rules: [{ required: true, message: "请输入硬件故障描述" }],
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
prop: "remark",
|
||||
label: "备注",
|
||||
componentProps: { type: "textarea", rows: 3, maxlength: 500 },
|
||||
colSpan: 24,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { CrudSchema } from '@/components/crud'
|
||||
import { ProduceRepairSettingService } from '@/apps/produce/api'
|
||||
|
||||
const settingTypeOptions = [
|
||||
{ label: '处理类型', value: 'type' },
|
||||
{ label: '维修原因', value: 'reason' },
|
||||
{ label: '具体问题', value: 'issue' },
|
||||
]
|
||||
|
||||
export const crudSchema = (): CrudSchema => ({
|
||||
api: ProduceRepairSettingService.list,
|
||||
dialogTitle: '维修设置',
|
||||
crudApi: {
|
||||
add: ProduceRepairSettingService.create,
|
||||
edit: ProduceRepairSettingService.update,
|
||||
remove: ProduceRepairSettingService.remove,
|
||||
view: ProduceRepairSettingService.get,
|
||||
},
|
||||
useCrud: true,
|
||||
hasAdd: true,
|
||||
hasEdit: true,
|
||||
hasRemove: true,
|
||||
hasView: true,
|
||||
addAuth: 'produce:repair-setting:create',
|
||||
editAuth: 'produce:repair-setting:update',
|
||||
removeAuth: 'produce:repair-setting:delete',
|
||||
viewAuth: 'produce:repair-setting:read',
|
||||
rowKey: 'id',
|
||||
searchFormSchema: {
|
||||
labelWidth: '90px',
|
||||
schema: [
|
||||
{
|
||||
prop: 'keyword', label: '关键字', component: 'Input',
|
||||
componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8,
|
||||
},
|
||||
{
|
||||
prop: 'setting_type', label: '设置层级', component: 'Select',
|
||||
componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8,
|
||||
},
|
||||
{
|
||||
prop: 'is_enabled', label: '启用状态', component: 'Select',
|
||||
componentProps: {
|
||||
clearable: true, placeholder: '请选择启用状态',
|
||||
options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }],
|
||||
}, colSpan: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{ type: 'selection' },
|
||||
{ type: 'index', label: '序号', width: 70 },
|
||||
{ prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' },
|
||||
{ prop: 'code', label: '编码', minWidth: 150 },
|
||||
{ prop: 'name', label: '名称', minWidth: 160 },
|
||||
{ prop: 'parent_name', label: '所属上级', minWidth: 140 },
|
||||
{ prop: 'sort', label: '排序', width: 90 },
|
||||
{ prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' },
|
||||
{ prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
|
||||
{ prop: 'created_name', label: '创建人', minWidth: 110 },
|
||||
],
|
||||
dialogProps: { width: '700px' },
|
||||
formSchema: {
|
||||
showDefaultActions: false,
|
||||
labelWidth: '105px',
|
||||
schema: [
|
||||
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
|
||||
{
|
||||
component: 'Select', prop: 'setting_type', label: '设置层级',
|
||||
componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions },
|
||||
rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect', prop: 'parent_id', label: '所属上级',
|
||||
componentProps: {
|
||||
api: '/produce/repair-setting', requestMethod: 'get',
|
||||
params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级',
|
||||
}, colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input', prop: 'code', label: '设置编码',
|
||||
componentProps: { placeholder: '请输入唯一编码', maxlength: 64 },
|
||||
rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input', prop: 'name', label: '设置名称',
|
||||
componentProps: { placeholder: '请输入显示名称', maxlength: 100 },
|
||||
rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12,
|
||||
},
|
||||
{ component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 },
|
||||
{
|
||||
component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1,
|
||||
componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input', prop: 'remark', label: '备注',
|
||||
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="produce-scan-data-page art-full-height">
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ProduceScanDataList' })
|
||||
|
||||
const [BasicCrud] = useCrud({
|
||||
...crudSchema(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.produce-scan-data-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -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": "刷新"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user