| | |
| | | }) |
| | | } |
| | | |
| | | export async function fetchExportBasStationAreaReport(payload = {}, options = {}) { |
| | | return fetch(`${import.meta.env.VITE_API_URL}/basStationArea/export`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...(options.headers || {}) |
| | | }, |
| | | body: JSON.stringify(payload) |
| | | }) |
| | | } |
| | | |
| | | export function fetchBasStationAreaQuery(condition = '') { |
| | | return request.post({ |
| | | url: '/basStationArea/query', |
| | |
| | | } |
| | | } |
| | | |
| | | if (params.orderId !== '' && params.orderId !== undefined && params.orderId !== null) { |
| | | const numericOrderId = Number(params.orderId) |
| | | if (!Number.isNaN(numericOrderId)) { |
| | | result.orderId = numericOrderId |
| | | } |
| | | } |
| | | |
| | | return result |
| | | } |
| | | |
| | |
| | | url: `/task/top/${id}` |
| | | }) |
| | | } |
| | | |
| | | export function fetchTaskAutoRunFlag() { |
| | | return request.get({ |
| | | url: '/config/flag/AUTO_RUN_CHECK_ORDERS' |
| | | }) |
| | | } |
| | | |
| | | export function fetchUpdateTaskAutoRunFlag(enabled) { |
| | | return request.post({ |
| | | url: '/config/byFlag', |
| | | params: { |
| | | val: !!enabled, |
| | | flag: 'AUTO_RUN_CHECK_ORDERS' |
| | | } |
| | | }) |
| | | } |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <RouterView v-else-if="isRefresh" v-slot="{ Component, route }" :style="contentStyle"> |
| | | <RouterView v-else-if="isRefresh" v-slot="{ Component, route }"> |
| | | <!-- 缓存路由动画 --> |
| | | <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear> |
| | | <div v-if="route.meta.keepAlive" class="art-page-view" :style="contentStyle"> |
| | | <KeepAlive :max="10" :exclude="keepAliveExclude"> |
| | | <component |
| | | class="art-page-view" |
| | | :is="Component" |
| | | :key="route.path" |
| | | v-if="route.meta.keepAlive" |
| | | /> |
| | | <component :is="Component" :key="route.path" /> |
| | | </KeepAlive> |
| | | </div> |
| | | </Transition> |
| | | |
| | | <!-- 非缓存路由动画 --> |
| | | <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear> |
| | | <component |
| | | class="art-page-view" |
| | | :is="Component" |
| | | :key="route.path" |
| | | v-if="!route.meta.keepAlive" |
| | | /> |
| | | <div v-if="!route.meta.keepAlive" class="art-page-view" :style="contentStyle"> |
| | | <component :is="Component" :key="route.path" /> |
| | | </div> |
| | | </Transition> |
| | | </RouterView> |
| | | |
| | |
| | | registerLocalIconCollections() |
| | | const app = createApp(App) |
| | | |
| | | // 注入错误日志面板用于调试 |
| | | app.config.errorHandler = (err, vm, info) => { |
| | | console.error("Vue Error:", err, info); |
| | | const div = document.createElement("div"); |
| | | div.style = "position:fixed;top:0;left:0;z-index:99999;background:red;color:white;padding:20px;font-size:16px;white-space:pre-wrap;width:100vw;height:100vh;overflow:auto;"; |
| | | div.innerText = "Error: " + (err.message || err) + "\n\nStack:\n" + err.stack + "\n\nInfo: " + info; |
| | | document.body.appendChild(div); |
| | | }; |
| | | window.addEventListener("error", (event) => { |
| | | const div = document.createElement("div"); |
| | | div.style = "position:fixed;top:0;left:0;z-index:99999;background:red;color:white;padding:20px;font-size:16px;white-space:pre-wrap;width:100vw;height:100vh;overflow:auto;"; |
| | | div.innerText = "Global Error: " + event.message + "\n\n" + event.error?.stack; |
| | | document.body.appendChild(div); |
| | | }); |
| | | |
| | | initStore(app) |
| | | initRouter(app) |
| | | setupGlobDirectives(app) |
| | |
| | | export function createBasStationAreaSearchState() { |
| | | return { |
| | | condition: '', |
| | | timeStart: '', |
| | | timeEnd: '', |
| | | stationAreaName: '', |
| | | stationAreaId: '', |
| | | type: '', |
| | |
| | | export function buildBasStationAreaSearchParams(params = {}) { |
| | | const searchParams = { |
| | | condition: normalizeText(params.condition), |
| | | timeStart: normalizeText(params.timeStart), |
| | | timeEnd: normalizeText(params.timeEnd), |
| | | stationAreaName: normalizeText(params.stationAreaName), |
| | | stationAreaId: normalizeText(params.stationAreaId), |
| | | type: |
| | |
| | | export function normalizeBasStationAreaListRow(record = {}, resolvers = {}) { |
| | | return normalizeBasStationAreaDetailRecord(record, resolvers) |
| | | } |
| | | |
| | | export function buildBasStationAreaPrintRows(records = [], resolvers = {}) { |
| | | if (!Array.isArray(records)) { |
| | | return [] |
| | | } |
| | | return records.map((record) => normalizeBasStationAreaListRow(record, resolvers)) |
| | | } |
| | | |
| | | export function buildBasStationAreaReportMeta({ |
| | | previewMeta = {}, |
| | | count = 0, |
| | | orientation = BAS_STATION_AREA_REPORT_STYLE.orientation |
| | | } = {}) { |
| | | return { |
| | | reportTitle: BAS_STATION_AREA_REPORT_TITLE, |
| | | reportDate: previewMeta.reportDate, |
| | | printedAt: previewMeta.printedAt, |
| | | operator: previewMeta.operator, |
| | | count, |
| | | reportStyle: { |
| | | ...BAS_STATION_AREA_REPORT_STYLE, |
| | | orientation |
| | | } |
| | | } |
| | | } |
| | |
| | | > |
| | | 批量删除 |
| | | </ElButton> |
| | | <ListExportPrint |
| | | class="inline-flex" |
| | | :preview-visible="previewVisible" |
| | | @update:previewVisible="handlePreviewVisibleChange" |
| | | :report-title="reportTitle" |
| | | :selected-rows="selectedRows" |
| | | :query-params="reportQueryParams" |
| | | :columns="columns" |
| | | :preview-rows="previewRows" |
| | | :preview-meta="resolvedPreviewMeta" |
| | | :total="pagination.total" |
| | | :disabled="loading" |
| | | @export="handleExport" |
| | | @print="handlePrint" |
| | | /> |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import ListExportPrint from '@/components/biz/list-export-print/index.vue' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { useCrudPage } from '@/views/system/common/useCrudPage' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchDictDataPage } from '@/api/system-manage' |
| | |
| | | import { fetchWarehouseAreasList } from '@/api/warehouse-areas' |
| | | import { |
| | | fetchBasStationAreaDetail, |
| | | fetchBasStationAreaMany, |
| | | fetchBasStationAreaPage, |
| | | fetchDeleteBasStationArea, |
| | | fetchExportBasStationAreaReport, |
| | | fetchSaveBasStationArea, |
| | | fetchUpdateBasStationArea |
| | | } from '@/api/bas-station-area' |
| | |
| | | import BasStationAreaDetailDrawer from './modules/bas-station-area-detail-drawer.vue' |
| | | import { createBasStationAreaTableColumns } from './basStationAreaTable.columns' |
| | | import { |
| | | BAS_STATION_AREA_REPORT_STYLE, |
| | | BAS_STATION_AREA_REPORT_TITLE, |
| | | buildBasStationAreaDialogModel, |
| | | buildBasStationAreaPageQueryParams, |
| | | buildBasStationAreaPrintRows, |
| | | buildBasStationAreaReportMeta, |
| | | buildBasStationAreaSavePayload, |
| | | buildBasStationAreaSearchParams, |
| | | createBasStationAreaSearchState, |
| | |
| | | defineOptions({ name: 'BasStationArea' }) |
| | | |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | |
| | | const searchForm = ref(createBasStationAreaSearchState()) |
| | | const detailDrawerVisible = ref(false) |
| | |
| | | const resolveTypeLabel = (value) => typeLabelMap.value.get(String(value)) || '' |
| | | const resolveStationAliasLabel = (id) => stationLabelMap.value.get(String(id)) || '' |
| | | const resolveUseStatusLabel = (value) => useStatusLabelMap.value.get(String(value)) || '' |
| | | const reportTitle = BAS_STATION_AREA_REPORT_TITLE |
| | | const reportQueryParams = computed(() => buildBasStationAreaSearchParams(searchForm.value)) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入站点区域名称/编号/备注' |
| | | } |
| | | }, |
| | | { |
| | | label: '开始时间', |
| | | key: 'timeStart', |
| | | type: 'date', |
| | | props: { |
| | | clearable: true, |
| | | type: 'date', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: '请选择开始时间' |
| | | } |
| | | }, |
| | | { |
| | | label: '结束时间', |
| | | key: 'timeEnd', |
| | | type: 'date', |
| | | props: { |
| | | clearable: true, |
| | | type: 'date', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: '请选择结束时间' |
| | | } |
| | | }, |
| | | { |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: '跨区区域', |
| | | key: 'crossZoneArea', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入跨区区域' |
| | | } |
| | | }, |
| | | { |
| | | label: '是否WCS', |
| | | key: 'isWcs', |
| | | type: 'select', |
| | | props: { |
| | | clearable: true, |
| | | options: getBasStationAreaBinaryOptions() |
| | | } |
| | | }, |
| | | { |
| | | label: 'WCS数据', |
| | | key: 'wcsData', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入WCS数据' |
| | | } |
| | | }, |
| | | { |
| | | label: '容器类型', |
| | | key: 'containerType', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入容器类型' |
| | | } |
| | | }, |
| | | { |
| | |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入条码' |
| | | } |
| | | }, |
| | | { |
| | | label: '站点别名', |
| | | key: 'stationAlias', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入站点别名' |
| | | } |
| | | }, |
| | | { |
| | |
| | | }) |
| | | handleDeleteAction = handleDelete |
| | | |
| | | const buildPreviewMeta = (rows) => { |
| | | const now = new Date() |
| | | return { |
| | | reportDate: now.toLocaleDateString('zh-CN'), |
| | | printedAt: now.toLocaleString('zh-CN', { hour12: false }), |
| | | operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '', |
| | | count: rows.length, |
| | | reportStyle: { ...BAS_STATION_AREA_REPORT_STYLE } |
| | | } |
| | | } |
| | | |
| | | const resolvePrintRecords = async (payload) => { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchBasStationAreaMany(payload.ids)).records |
| | | } |
| | | return defaultResponseAdapter( |
| | | await fetchBasStationAreaPage({ |
| | | ...reportQueryParams.value, |
| | | current: 1, |
| | | pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20 |
| | | }) |
| | | ).records |
| | | } |
| | | |
| | | const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } = |
| | | usePrintExportPage({ |
| | | downloadFileName: 'bas-station-area.xlsx', |
| | | requestExport: (payload) => |
| | | fetchExportBasStationAreaReport(payload, { |
| | | headers: { |
| | | Authorization: userStore.accessToken || '' |
| | | } |
| | | }), |
| | | resolvePrintRecords, |
| | | buildPreviewRows: (records) => |
| | | buildBasStationAreaPrintRows(records, { |
| | | resolveAreaLabel, |
| | | resolveCrossZoneAreaLabel, |
| | | resolveContainerTypeLabel, |
| | | resolveTypeLabel, |
| | | resolveStationAliasLabel, |
| | | resolveUseStatusLabel |
| | | }), |
| | | buildPreviewMeta |
| | | }) |
| | | |
| | | const resolvedPreviewMeta = computed(() => |
| | | buildBasStationAreaReportMeta({ |
| | | previewMeta: previewMeta.value, |
| | | count: previewRows.value.length, |
| | | orientation: previewMeta.value?.reportStyle?.orientation || BAS_STATION_AREA_REPORT_STYLE.orientation |
| | | }) |
| | | ) |
| | | |
| | | function handleSearch(params) { |
| | | replaceSearchParams(buildBasStationAreaSearchParams(params)) |
| | | getData() |
| | |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="流程图查看" |
| | | size="900px" |
| | | size="92%" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | > |
| | | <ElScrollbar class="h-[calc(100vh-180px)] pr-1"> |
| | | <div v-if="loading" class="py-6"> |
| | | <ElSkeleton :rows="10" animated /> |
| | | <div class="flex h-[calc(100vh-160px)] flex-col gap-4"> |
| | | <ElCard shadow="never" class="shrink-0"> |
| | | <div class="flex items-start justify-between gap-4"> |
| | | <div class="min-w-0"> |
| | | <div class="text-base font-semibold text-[var(--art-text-primary)]"> |
| | | {{ detail.templateName || detail.templateCode || '--' }} |
| | | </div> |
| | | <div v-else class="space-y-4"> |
| | | <ElCard shadow="never" class="art-table-card"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div> |
| | | <h3 class="m-0 text-base font-semibold">模板流程快照</h3> |
| | | <p class="m-0 text-sm text-[var(--art-text-secondary)]"> |
| | | 这里展示的是后端模板字段组合出的真实流程信息,不做额外假数据推演。 |
| | | </p> |
| | | <div class="mt-1 text-sm text-[var(--art-text-secondary)]"> |
| | | 模板编码 {{ detail.templateCode || '--' }},起点 {{ detail.sourceType || '--' }},终点 |
| | | {{ detail.targetType || '--' }} |
| | | </div> |
| | | </div> |
| | | <ElSpace wrap> |
| | | <ElTag :type="detail.statusType || 'info'" effect="light"> |
| | | {{ detail.statusText || '--' }} |
| | | </ElTag> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="grid gap-3 md:grid-cols-4"> |
| | | <div |
| | | v-for="item in flowSnapshot" |
| | | :key="item.key" |
| | | class="rounded-lg border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4" |
| | | > |
| | | <div class="text-sm text-[var(--art-text-secondary)]">{{ item.title }}</div> |
| | | <div class="mt-2 text-base font-semibold text-[var(--art-text-primary)]"> |
| | | {{ item.value }} |
| | | </div> |
| | | </div> |
| | | <ElTag :type="detail.isCurrentType || 'info'" effect="light"> |
| | | {{ detail.isCurrentText || '--' }} |
| | | </ElTag> |
| | | </ElSpace> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ElDescriptions title="流程依据" :column="2" border> |
| | | <ElDescriptionsItem label="模板编码">{{ detail.templateCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="模板名称">{{ detail.templateName || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="起点类型">{{ detail.sourceType || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="终点类型">{{ detail.targetType || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="步序长度">{{ detail.stepSize ?? '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="优先级">{{ detail.priority ?? '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | <div class="grid min-h-0 flex-1 gap-4 xl:grid-cols-3"> |
| | | <ElCard shadow="never" class="min-h-0"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <span class="font-medium">模板节点</span> |
| | | <span class="text-xs text-[var(--art-text-secondary)]"> |
| | | {{ nodeLoading ? '加载中' : `第 ${nodePagination.current} 页 / 共 ${nodePagination.total} 条` }} |
| | | </span> |
| | | </div> |
| | | </ElScrollbar> |
| | | </template> |
| | | |
| | | <div class="flex h-[calc(100vh-300px)] flex-col"> |
| | | <ElSkeleton v-if="loading || nodeLoading" :rows="8" animated /> |
| | | <ElEmpty |
| | | v-else-if="nodeRows.length === 0" |
| | | description="暂无节点数据" |
| | | :image-size="100" |
| | | /> |
| | | <template v-else> |
| | | <ElTable |
| | | :data="nodeRows" |
| | | border |
| | | highlight-current-row |
| | | height="100%" |
| | | :current-row-key="selectedNodeId" |
| | | row-key="id" |
| | | @current-change="handleNodeClick" |
| | | > |
| | | <ElTableColumn prop="nodeOrder" label="顺序" width="72" align="center" /> |
| | | <ElTableColumn prop="nodeCode" label="节点编码" min-width="140" show-overflow-tooltip /> |
| | | <ElTableColumn prop="nodeName" label="节点名称" min-width="160" show-overflow-tooltip /> |
| | | <ElTableColumn prop="systemCode" label="系统编码" min-width="140" show-overflow-tooltip /> |
| | | </ElTable> |
| | | <div class="mt-3 flex justify-end"> |
| | | <ElPagination |
| | | small |
| | | background |
| | | layout="prev, pager, next" |
| | | :current-page="nodePagination.current" |
| | | :page-size="nodePagination.pageSize" |
| | | :total="nodePagination.total" |
| | | @current-change="handleNodePageChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ElCard shadow="never" class="min-h-0"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <span class="font-medium">子系统流程</span> |
| | | <span class="text-xs text-[var(--art-text-secondary)]"> |
| | | {{ |
| | | flowLoading |
| | | ? '加载中' |
| | | : selectedNodeId |
| | | ? `第 ${flowPagination.current} 页 / 共 ${flowPagination.total} 条` |
| | | : '待选择节点' |
| | | }} |
| | | </span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex h-[calc(100vh-300px)] flex-col"> |
| | | <div |
| | | v-if="!selectedNodeId" |
| | | class="flex h-full items-center justify-center text-sm text-[var(--art-text-secondary)]" |
| | | > |
| | | 请先选择左侧模板节点 |
| | | </div> |
| | | <ElSkeleton v-else-if="flowLoading" :rows="8" animated /> |
| | | <ElEmpty |
| | | v-else-if="flowRows.length === 0" |
| | | description="暂无流程数据" |
| | | :image-size="100" |
| | | /> |
| | | <template v-else> |
| | | <ElTable |
| | | :data="flowRows" |
| | | border |
| | | highlight-current-row |
| | | height="100%" |
| | | :current-row-key="selectedFlowId" |
| | | row-key="id" |
| | | @current-change="handleFlowClick" |
| | | > |
| | | <ElTableColumn prop="flowCode" label="流程编码" min-width="160" show-overflow-tooltip /> |
| | | <ElTableColumn prop="flowName" label="流程名称" min-width="180" show-overflow-tooltip /> |
| | | <ElTableColumn prop="systemCode" label="系统编码" min-width="140" show-overflow-tooltip /> |
| | | </ElTable> |
| | | <div class="mt-3 flex justify-end"> |
| | | <ElPagination |
| | | small |
| | | background |
| | | layout="prev, pager, next" |
| | | :current-page="flowPagination.current" |
| | | :page-size="flowPagination.pageSize" |
| | | :total="flowPagination.total" |
| | | @current-change="handleFlowPageChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ElCard shadow="never" class="min-h-0"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <span class="font-medium">流程步骤</span> |
| | | <span class="text-xs text-[var(--art-text-secondary)]"> |
| | | {{ |
| | | stepLoading |
| | | ? '加载中' |
| | | : selectedFlowId |
| | | ? `第 ${stepPagination.current} 页 / 共 ${stepPagination.total} 条` |
| | | : '待选择流程' |
| | | }} |
| | | </span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex h-[calc(100vh-300px)] flex-col"> |
| | | <div |
| | | v-if="!selectedFlowId" |
| | | class="flex h-full items-center justify-center text-sm text-[var(--art-text-secondary)]" |
| | | > |
| | | 请先选择中间子系统流程 |
| | | </div> |
| | | <ElSkeleton v-else-if="stepLoading" :rows="8" animated /> |
| | | <ElEmpty |
| | | v-else-if="stepRows.length === 0" |
| | | description="暂无步骤数据" |
| | | :image-size="100" |
| | | /> |
| | | <template v-else> |
| | | <ElTable :data="stepRows" border height="100%" row-key="id"> |
| | | <ElTableColumn prop="stepOrder" label="顺序" width="72" align="center" /> |
| | | <ElTableColumn prop="stepCode" label="步骤编码" min-width="140" show-overflow-tooltip /> |
| | | <ElTableColumn prop="stepName" label="步骤名称" min-width="180" show-overflow-tooltip /> |
| | | <ElTableColumn prop="stepType" label="步骤类型" min-width="140" show-overflow-tooltip /> |
| | | </ElTable> |
| | | <div class="mt-3 flex justify-end"> |
| | | <ElPagination |
| | | small |
| | | background |
| | | layout="prev, pager, next" |
| | | :current-page="stepPagination.current" |
| | | :page-size="stepPagination.pageSize" |
| | | :total="stepPagination.total" |
| | | @current-change="handleStepPageChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </ElCard> |
| | | </div> |
| | | </div> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from 'vue' |
| | | import { buildTaskPathTemplateFlowSnapshot } from '../taskPathTemplatePage.helpers' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { |
| | | fetchTaskPathTemplateNodePage |
| | | } from '@/api/task-path-template-node' |
| | | import { fetchSubsystemFlowTemplatePage } from '@/api/subsystem-flow-template' |
| | | import { fetchFlowStepTemplatePage } from '@/api/flow-step-template' |
| | | |
| | | const props = defineProps({ |
| | | visible: { type: Boolean, default: false }, |
| | |
| | | set: (value) => emit('update:visible', value) |
| | | }) |
| | | |
| | | const flowSnapshot = computed(() => buildTaskPathTemplateFlowSnapshot(props.detail)) |
| | | const nodeLoading = ref(false) |
| | | const flowLoading = ref(false) |
| | | const stepLoading = ref(false) |
| | | const nodeRows = ref([]) |
| | | const flowRows = ref([]) |
| | | const stepRows = ref([]) |
| | | const selectedNodeId = ref(null) |
| | | const selectedFlowId = ref(null) |
| | | const DEFAULT_PAGE_SIZE = 20 |
| | | const nodePagination = ref({ current: 1, pageSize: DEFAULT_PAGE_SIZE, total: 0 }) |
| | | const flowPagination = ref({ current: 1, pageSize: DEFAULT_PAGE_SIZE, total: 0 }) |
| | | const stepPagination = ref({ current: 1, pageSize: DEFAULT_PAGE_SIZE, total: 0 }) |
| | | |
| | | function normalizeRecords(response) { |
| | | return Array.isArray(response?.records) ? response.records : [] |
| | | } |
| | | |
| | | function normalizeTotal(response) { |
| | | const total = Number(response?.total) |
| | | return Number.isNaN(total) ? 0 : total |
| | | } |
| | | |
| | | function resetFlowState() { |
| | | nodeRows.value = [] |
| | | flowRows.value = [] |
| | | stepRows.value = [] |
| | | selectedNodeId.value = null |
| | | selectedFlowId.value = null |
| | | nodePagination.value.current = 1 |
| | | nodePagination.value.total = 0 |
| | | flowPagination.value.current = 1 |
| | | flowPagination.value.total = 0 |
| | | stepPagination.value.current = 1 |
| | | stepPagination.value.total = 0 |
| | | } |
| | | |
| | | async function loadStepRows(flowId, current = stepPagination.value.current) { |
| | | if (!flowId) { |
| | | stepRows.value = [] |
| | | selectedFlowId.value = null |
| | | stepPagination.value.total = 0 |
| | | return |
| | | } |
| | | |
| | | stepLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchFlowStepTemplatePage({ |
| | | current, |
| | | pageSize: stepPagination.value.pageSize, |
| | | flowId |
| | | }), |
| | | { records: [] }, |
| | | { timeoutMessage: '流程步骤加载超时,已停止等待' } |
| | | ) |
| | | stepRows.value = normalizeRecords(response) |
| | | stepPagination.value.current = current |
| | | stepPagination.value.total = normalizeTotal(response) |
| | | } catch (error) { |
| | | stepRows.value = [] |
| | | stepPagination.value.total = 0 |
| | | ElMessage.error(error?.message || '流程步骤加载失败') |
| | | } finally { |
| | | stepLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function loadFlowRows(node, current = flowPagination.value.current) { |
| | | if (!node?.nodeCode) { |
| | | flowRows.value = [] |
| | | stepRows.value = [] |
| | | selectedFlowId.value = null |
| | | flowPagination.value.total = 0 |
| | | stepPagination.value.total = 0 |
| | | return |
| | | } |
| | | |
| | | flowLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchSubsystemFlowTemplatePage({ |
| | | current, |
| | | pageSize: flowPagination.value.pageSize, |
| | | flowCode: node.nodeCode |
| | | }), |
| | | { records: [] }, |
| | | { timeoutMessage: '子系统流程加载超时,已停止等待' } |
| | | ) |
| | | flowRows.value = normalizeRecords(response) |
| | | flowPagination.value.current = current |
| | | flowPagination.value.total = normalizeTotal(response) |
| | | selectedFlowId.value = null |
| | | stepRows.value = [] |
| | | stepPagination.value.current = 1 |
| | | stepPagination.value.total = 0 |
| | | } catch (error) { |
| | | flowRows.value = [] |
| | | stepRows.value = [] |
| | | selectedFlowId.value = null |
| | | flowPagination.value.total = 0 |
| | | stepPagination.value.total = 0 |
| | | ElMessage.error(error?.message || '子系统流程加载失败') |
| | | } finally { |
| | | flowLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function loadNodeRows(templateId, current = nodePagination.value.current) { |
| | | if (!templateId) { |
| | | resetFlowState() |
| | | return |
| | | } |
| | | |
| | | nodeLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchTaskPathTemplateNodePage({ |
| | | current, |
| | | pageSize: nodePagination.value.pageSize, |
| | | templateId |
| | | }), |
| | | { records: [] }, |
| | | { timeoutMessage: '模板节点加载超时,已停止等待' } |
| | | ) |
| | | nodeRows.value = normalizeRecords(response) |
| | | nodePagination.value.current = current |
| | | nodePagination.value.total = normalizeTotal(response) |
| | | selectedNodeId.value = null |
| | | flowRows.value = [] |
| | | stepRows.value = [] |
| | | flowPagination.value.current = 1 |
| | | flowPagination.value.total = 0 |
| | | stepPagination.value.current = 1 |
| | | stepPagination.value.total = 0 |
| | | } catch (error) { |
| | | resetFlowState() |
| | | ElMessage.error(error?.message || '模板节点加载失败') |
| | | } finally { |
| | | nodeLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleNodeClick(node) { |
| | | if (!node || selectedNodeId.value === node.id) return |
| | | selectedNodeId.value = node.id |
| | | selectedFlowId.value = null |
| | | stepRows.value = [] |
| | | flowPagination.value.current = 1 |
| | | stepPagination.value.current = 1 |
| | | loadFlowRows(node) |
| | | } |
| | | |
| | | function handleFlowClick(flow) { |
| | | if (!flow || selectedFlowId.value === flow.id) return |
| | | selectedFlowId.value = flow.id |
| | | stepPagination.value.current = 1 |
| | | loadStepRows(flow.id) |
| | | } |
| | | |
| | | function handleNodePageChange(current) { |
| | | loadNodeRows(props.detail?.id, current) |
| | | } |
| | | |
| | | function handleFlowPageChange(current) { |
| | | const node = nodeRows.value.find((item) => item.id === selectedNodeId.value) |
| | | if (!node) return |
| | | loadFlowRows(node, current) |
| | | } |
| | | |
| | | function handleStepPageChange(current) { |
| | | if (!selectedFlowId.value) return |
| | | loadStepRows(selectedFlowId.value, current) |
| | | } |
| | | |
| | | function handleVisibleChange(value) { |
| | | visible.value = value |
| | | } |
| | | |
| | | watch( |
| | | () => [props.visible, props.detail?.id], |
| | | ([isVisible, detailId]) => { |
| | | if (isVisible && detailId) { |
| | | loadNodeRows(detailId) |
| | | } |
| | | if (!isVisible) { |
| | | resetFlowState() |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | |
| | | > |
| | | 批量删除 |
| | | </ElButton> |
| | | <ListExportPrint |
| | | class="inline-flex" |
| | | :preview-visible="previewVisible" |
| | | @update:previewVisible="handlePreviewVisibleChange" |
| | | :report-title="reportTitle" |
| | | :selected-rows="selectedRows" |
| | | :query-params="reportQueryParams" |
| | | :columns="columns" |
| | | :preview-rows="previewRows" |
| | | :preview-meta="resolvedPreviewMeta" |
| | | :total="pagination.total" |
| | | :disabled="loading" |
| | | @export="handleExport" |
| | | @print="handlePrint" |
| | | /> |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import ListExportPrint from '@/components/biz/list-export-print/index.vue' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { useTableColumns } from '@/hooks/core/useTableColumns' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useCrudPage } from '@/views/system/common/useCrudPage' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchDictDataPage } from '@/api/system-manage' |
| | |
| | | fetchCompanysList, |
| | | fetchDeleteWarehouseAreas, |
| | | fetchWarehouseAreasDetail, |
| | | fetchWarehouseAreasMany, |
| | | fetchWarehouseAreasPage, |
| | | fetchExportWarehouseAreasReport, |
| | | fetchSaveWarehouseAreas, |
| | | fetchUpdateWarehouseAreas, |
| | | fetchWarehouseList |
| | |
| | | import { |
| | | buildWarehouseAreasDialogModel, |
| | | buildWarehouseAreasPageQueryParams, |
| | | buildWarehouseAreasPrintRows, |
| | | buildWarehouseAreasSavePayload, |
| | | buildWarehouseAreasSearchParams, |
| | | createWarehouseAreasSearchState, |
| | | getWarehouseAreasPaginationKey, |
| | | WAREHOUSE_AREAS_REPORT_STYLE, |
| | | WAREHOUSE_AREAS_REPORT_TITLE, |
| | | getWarehouseAreasStatusOptions, |
| | | normalizeWarehouseAreasDetailRecord, |
| | | normalizeWarehouseAreasListRow |
| | |
| | | defineOptions({ name: 'WarehouseAreas' }) |
| | | |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | |
| | | const searchForm = ref(createWarehouseAreasSearchState()) |
| | | const detailDrawerVisible = ref(false) |
| | |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | const reportTitle = WAREHOUSE_AREAS_REPORT_TITLE |
| | | const reportQueryParams = computed(() => buildWarehouseAreasSearchParams(searchForm.value)) |
| | | |
| | | async function openDetail(row) { |
| | | detailDrawerVisible.value = true |
| | |
| | | }) |
| | | handleDeleteAction = handleDelete |
| | | |
| | | const buildPreviewMeta = (rows) => { |
| | | const now = new Date() |
| | | return { |
| | | reportDate: now.toLocaleDateString('zh-CN'), |
| | | printedAt: now.toLocaleString('zh-CN', { hour12: false }), |
| | | operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '', |
| | | count: rows.length, |
| | | reportStyle: { ...WAREHOUSE_AREAS_REPORT_STYLE } |
| | | } |
| | | } |
| | | |
| | | const resolvePrintRecords = async (payload) => { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchWarehouseAreasMany(payload.ids)).records |
| | | } |
| | | return defaultResponseAdapter( |
| | | await fetchWarehouseAreasPage({ |
| | | ...reportQueryParams.value, |
| | | current: 1, |
| | | pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20 |
| | | }) |
| | | ).records |
| | | } |
| | | |
| | | const { |
| | | previewVisible, |
| | | previewRows, |
| | | previewMeta, |
| | | handlePreviewVisibleChange, |
| | | handleExport, |
| | | handlePrint |
| | | } = usePrintExportPage({ |
| | | downloadFileName: 'warehouse-areas.xlsx', |
| | | requestExport: (payload) => |
| | | fetchExportWarehouseAreasReport(payload, { |
| | | headers: { |
| | | Authorization: userStore.accessToken || '' |
| | | } |
| | | }), |
| | | resolvePrintRecords, |
| | | buildPreviewRows: (records) => buildWarehouseAreasPrintRows(records), |
| | | buildPreviewMeta |
| | | }) |
| | | |
| | | const resolvedPreviewMeta = computed(() => ({ |
| | | ...previewMeta.value, |
| | | reportTitle, |
| | | count: previewRows.value.length || previewMeta.value?.count || 0, |
| | | reportStyle: { |
| | | ...WAREHOUSE_AREAS_REPORT_STYLE, |
| | | ...(previewMeta.value?.reportStyle || {}) |
| | | } |
| | | })) |
| | | |
| | | function handleSearch(params) { |
| | | replaceSearchParams(buildWarehouseAreasSearchParams(params)) |
| | | getData() |
| | |
| | | /> |
| | | |
| | | <ElCard class="art-table-card"> |
| | | <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" /> |
| | | <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData"> |
| | | <template #left> |
| | | <ElSpace wrap> |
| | | <ElButton |
| | | v-if="!autoRunEnabled" |
| | | type="primary" |
| | | plain |
| | | :loading="autoRunLoading" |
| | | @click="handleToggleAutoRun(true)" |
| | | > |
| | | 自动下发任务 |
| | | </ElButton> |
| | | <ElButton |
| | | v-else |
| | | type="warning" |
| | | plain |
| | | :loading="autoRunLoading" |
| | | @click="handleToggleAutoRun(false)" |
| | | > |
| | | 暂停自动下发 |
| | | </ElButton> |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | | |
| | | <ArtTable |
| | | :loading="loading" |
| | |
| | | /> |
| | | </ElCard> |
| | | |
| | | <TaskFlowStepDialog |
| | | v-model:visible="flowStepDialogVisible" |
| | | :task-row="activeTaskRow" |
| | | /> |
| | | |
| | | <TaskDetailDrawer |
| | | v-model:visible="detailDrawerVisible" |
| | | :loading="detailLoading" |
| | |
| | | @refresh="loadDetailResources" |
| | | @size-change="handleDetailSizeChange" |
| | | @current-change="handleDetailCurrentChange" |
| | | @flow-step="handleOpenFlowStepFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue' |
| | | import { useTableColumns } from '@/hooks/core/useTableColumns' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { |
| | |
| | | fetchCompleteTask, |
| | | fetchPickTask, |
| | | fetchRemoveTask, |
| | | fetchTaskDetail, |
| | | fetchTaskAutoRunFlag, |
| | | fetchTaskItemPage, |
| | | fetchTaskPage, |
| | | fetchTopTask |
| | | fetchTopTask, |
| | | fetchUpdateTaskAutoRunFlag |
| | | } from '@/api/task' |
| | | import TaskDetailDrawer from './modules/task-detail-drawer.vue' |
| | | import TaskExpandPanel from './modules/task-expand-panel.vue' |
| | | import TaskFlowStepDialog from './modules/task-flow-step-dialog.vue' |
| | | import { createTaskTableColumns } from './taskTable.columns' |
| | | import { |
| | | buildTaskPageQueryParams, |
| | |
| | | const detailData = ref({}) |
| | | const detailTableData = ref([]) |
| | | const activeTaskRow = ref(null) |
| | | const flowStepDialogVisible = ref(false) |
| | | const autoRunEnabled = ref(false) |
| | | const autoRunLoading = ref(false) |
| | | let refreshTimer = null |
| | | |
| | | const pagination = reactive({ |
| | | current: 1, |
| | |
| | | |
| | | async function openDetailDrawer(row) { |
| | | activeTaskRow.value = row |
| | | detailData.value = normalizeTaskRow(row) |
| | | detailPagination.current = 1 |
| | | detailDrawerVisible.value = true |
| | | await loadDetailResources() |
| | | } |
| | | |
| | | function handleOpenFlowStepFromDetail() { |
| | | if (!activeTaskRow.value) { |
| | | return |
| | | } |
| | | flowStepDialogVisible.value = true |
| | | } |
| | | |
| | | async function handleActionClick(action, row) { |
| | | try { |
| | | if (action.key === 'view') { |
| | | await openDetailDrawer(row) |
| | | return |
| | | } |
| | | |
| | | if (action.key === 'flowStep') { |
| | | activeTaskRow.value = row |
| | | flowStepDialogVisible.value = true |
| | | return |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | const { columns, columnChecks } = useTableColumns(() => createTaskTableColumns(handleActionClick)) |
| | | const { columns, columnChecks } = useTableColumns(() => |
| | | createTaskTableColumns({ |
| | | handleActionClick, |
| | | createExpandContent: (row) => ({ |
| | | name: 'TaskExpandPanelHost', |
| | | render() { |
| | | return h(TaskExpandPanel, { row }) |
| | | } |
| | | }) |
| | | }) |
| | | ) |
| | | |
| | | function updatePaginationState(target, response, fallbackCurrent, fallbackSize) { |
| | | target.total = Number(response?.total || 0) |
| | |
| | | } |
| | | } |
| | | |
| | | async function loadAutoRunConfig() { |
| | | autoRunLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage(fetchTaskAutoRunFlag(), { val: false }, { |
| | | timeoutMessage: '自动下发配置加载超时,已停止等待' |
| | | }) |
| | | const rawValue = response?.val |
| | | autoRunEnabled.value = |
| | | rawValue === true || rawValue === 'true' || rawValue === 1 || rawValue === '1' |
| | | } finally { |
| | | autoRunLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleToggleAutoRun(enabled) { |
| | | autoRunLoading.value = true |
| | | try { |
| | | await fetchUpdateTaskAutoRunFlag(enabled) |
| | | autoRunEnabled.value = enabled |
| | | ElMessage.success(enabled ? '已开启自动下发任务' : '已暂停自动下发任务') |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '自动下发配置更新失败') |
| | | } finally { |
| | | autoRunLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function loadDetailResources() { |
| | | if (!activeTaskRow.value?.id) { |
| | | return |
| | |
| | | |
| | | detailLoading.value = true |
| | | try { |
| | | const [detailResponse, taskItemResponse] = await Promise.all([ |
| | | guardRequestWithMessage(fetchTaskDetail(activeTaskRow.value.id), {}, { |
| | | timeoutMessage: '任务详情加载超时,已停止等待' |
| | | }), |
| | | guardRequestWithMessage( |
| | | const taskItemResponse = await guardRequestWithMessage( |
| | | fetchTaskItemPage({ |
| | | taskId: activeTaskRow.value.id, |
| | | current: detailPagination.current, |
| | |
| | | }, |
| | | { timeoutMessage: '任务明细加载超时,已停止等待' } |
| | | ) |
| | | ]) |
| | | |
| | | detailData.value = normalizeTaskRow(detailResponse) |
| | | detailData.value = normalizeTaskRow(activeTaskRow.value) |
| | | detailTableData.value = Array.isArray(taskItemResponse?.records) |
| | | ? taskItemResponse.records.map((record) => normalizeTaskItemRow(record)) |
| | | : [] |
| | | updatePaginationState(detailPagination, taskItemResponse, detailPagination.current, detailPagination.size) |
| | | } catch (error) { |
| | | detailTableData.value = [] |
| | | ElMessage.error(error?.message || '任务明细加载失败') |
| | | } finally { |
| | | detailLoading.value = false |
| | | } |
| | |
| | | loadDetailResources() |
| | | } |
| | | |
| | | onMounted(loadPageData) |
| | | function startAutoRefresh() { |
| | | clearAutoRefresh() |
| | | refreshTimer = window.setInterval(() => { |
| | | void loadPageData() |
| | | }, 5000) |
| | | } |
| | | |
| | | function clearAutoRefresh() { |
| | | if (refreshTimer) { |
| | | window.clearInterval(refreshTimer) |
| | | refreshTimer = null |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await Promise.all([loadPageData(), loadAutoRunConfig()]) |
| | | startAutoRefresh() |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | clearAutoRefresh() |
| | | }) |
| | | </script> |
| | |
| | | @update:model-value="handleVisibleChange" |
| | | > |
| | | <div class="flex h-full flex-col gap-4"> |
| | | <ElDescriptions :column="4" border> |
| | | <ElDescriptionsItem label="任务号">{{ detail.taskCode || '--' }}</ElDescriptionsItem> |
| | | <div class="grid gap-4 xl:grid-cols-[1.45fr_1fr]"> |
| | | <ElCard shadow="never" class="border border-[var(--el-border-color-lighter)]"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">任务基础信息</span> |
| | | <ElTag size="small" effect="plain" type="primary"> |
| | | {{ detail.taskCode || '--' }} |
| | | </ElTag> |
| | | </div> |
| | | </template> |
| | | |
| | | <ElDescriptions :column="2" border> |
| | | <ElDescriptionsItem label="任务状态">{{ detail.taskStatusLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="任务类型">{{ detail.taskTypeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="设备类型">{{ detail.warehTypeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="优先级">{{ detail.sort ?? '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="状态">{{ detail.statusText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="机器人编码">{{ detail.robotCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="备注" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | </ElCard> |
| | | |
| | | <ElCard shadow="never" class="border border-[var(--el-border-color-lighter)]"> |
| | | <template #header> |
| | | <div class="flex items-center justify-between"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">执行路径</span> |
| | | <ElButton text type="primary" @click="$emit('flow-step')">流程步骤</ElButton> |
| | | </div> |
| | | </template> |
| | | |
| | | <ElDescriptions :column="1" border> |
| | | <ElDescriptionsItem label="源库位">{{ detail.orgLoc || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="源站点">{{ detail.orgSiteLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="目标库位">{{ detail.targLoc || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="目标站点">{{ detail.targSiteLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="托盘码">{{ detail.barcode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="机器人编码">{{ detail.robotCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="优先级">{{ detail.sort ?? '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="状态">{{ detail.statusText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="备注" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | </ElCard> |
| | | </div> |
| | | |
| | | <div class="flex items-center justify-between"> |
| | | <div class="text-sm text-[var(--art-gray-600)]">任务明细</div> |
| | | <div> |
| | | <div class="text-sm font-medium text-[var(--art-text-gray-900)]">任务明细</div> |
| | | <div class="mt-1 text-xs text-[var(--art-text-gray-500)]"> |
| | | 查看当前任务关联的业务单据、物料和执行记录 |
| | | </div> |
| | | </div> |
| | | <div class="flex items-center gap-2"> |
| | | <ElButton :loading="loading" @click="$emit('refresh')">刷新</ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <ArtTable |
| | |
| | | pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change']) |
| | | const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change', 'flow-step']) |
| | | |
| | | function handleVisibleChange(visible) { |
| | | emit('update:visible', visible) |
| New file |
| | |
| | | <template> |
| | | <div class="rounded-xl bg-[var(--el-fill-color-blank)] px-4 py-4"> |
| | | <div class="mb-3 flex items-center justify-between"> |
| | | <div class="text-sm font-medium text-[var(--art-gray-900)]">任务明细</div> |
| | | <ElButton text size="small" :loading="loading" @click="loadData">刷新</ElButton> |
| | | </div> |
| | | |
| | | <ArtTable |
| | | :loading="loading" |
| | | :data="rows" |
| | | :columns="columns" |
| | | empty-text="暂无任务明细" |
| | | :empty-height="'220px'" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, watch } from 'vue' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchTaskItemPage } from '@/api/task' |
| | | import { normalizeTaskItemRow } from '../taskPage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | row: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const loading = ref(false) |
| | | const rows = ref([]) |
| | | |
| | | const columns = [ |
| | | { |
| | | type: 'globalIndex', |
| | | label: '序号', |
| | | width: 72, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | prop: 'orderTypeLabel', |
| | | label: '单据类型', |
| | | minWidth: 120, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'wkTypeLabel', |
| | | label: '业务类型', |
| | | minWidth: 120, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'platWorkCode', |
| | | label: '工单号', |
| | | minWidth: 150, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'platItemId', |
| | | label: '行号', |
| | | minWidth: 100, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'matnrCode', |
| | | label: '物料编码', |
| | | minWidth: 150, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'maktx', |
| | | label: '物料名称', |
| | | minWidth: 220, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'batch', |
| | | label: '批次', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'unit', |
| | | label: '单位', |
| | | width: 100 |
| | | }, |
| | | { |
| | | prop: 'anfme', |
| | | label: '数量', |
| | | width: 100, |
| | | align: 'right' |
| | | }, |
| | | { |
| | | prop: 'updateByText', |
| | | label: '更新人', |
| | | minWidth: 120, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'updateTimeText', |
| | | label: '更新时间', |
| | | minWidth: 180, |
| | | showOverflowTooltip: true |
| | | } |
| | | ] |
| | | |
| | | async function loadData() { |
| | | if (!props.row?.id) { |
| | | rows.value = [] |
| | | return |
| | | } |
| | | |
| | | loading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchTaskItemPage({ |
| | | taskId: props.row.id, |
| | | current: 1, |
| | | pageSize: 50 |
| | | }), |
| | | { records: [] }, |
| | | { timeoutMessage: '任务明细加载超时,已停止等待' } |
| | | ) |
| | | rows.value = Array.isArray(response?.records) |
| | | ? response.records.map((record) => normalizeTaskItemRow(record)) |
| | | : [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => props.row?.id, |
| | | () => { |
| | | void loadData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | void loadData() |
| | | }) |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <ElDialog |
| | | :model-value="visible" |
| | | width="80%" |
| | | top="6vh" |
| | | destroy-on-close |
| | | title="流程步骤" |
| | | @update:model-value="emit('update:visible', $event)" |
| | | > |
| | | <div class="flex flex-col gap-4"> |
| | | <div class="rounded-xl border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-blank)] px-4 py-3"> |
| | | <div class="text-sm text-[var(--art-gray-500)]">当前任务</div> |
| | | <div class="mt-1 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-[var(--art-gray-900)]"> |
| | | <span>任务号:{{ taskRow?.taskCode || '--' }}</span> |
| | | <span>任务状态:{{ taskRow?.taskStatusLabel || '--' }}</span> |
| | | <span>任务类型:{{ taskRow?.taskTypeLabel || '--' }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <ArtTable |
| | | :loading="loading" |
| | | :data="rows" |
| | | :columns="columns" |
| | | :pagination="pagination" |
| | | @pagination:size-change="handleSizeChange" |
| | | @pagination:current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </ElDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchFlowStepInstancePage } from '@/api/flow-step-instance' |
| | | import { normalizeFlowStepInstanceRow } from '@/views/system/flow-step-instance/flowStepInstancePage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | taskRow: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible']) |
| | | |
| | | const loading = ref(false) |
| | | const rows = ref([]) |
| | | const pagination = reactive({ |
| | | current: 1, |
| | | size: 20, |
| | | total: 0 |
| | | }) |
| | | |
| | | const columns = computed(() => [ |
| | | { |
| | | type: 'globalIndex', |
| | | label: '序号', |
| | | width: 72, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | prop: 'flowInstanceNo', |
| | | label: '流程实例号', |
| | | minWidth: 160, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'stepCode', |
| | | label: '步骤编码', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'stepName', |
| | | label: '步骤名称', |
| | | minWidth: 180, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'stepType', |
| | | label: '步骤类型', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'executeResult', |
| | | label: '执行结果', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'statusText', |
| | | label: '状态', |
| | | width: 120, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'startTimeText', |
| | | label: '开始时间', |
| | | minWidth: 180, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'endTimeText', |
| | | label: '结束时间', |
| | | minWidth: 180, |
| | | showOverflowTooltip: true |
| | | } |
| | | ]) |
| | | |
| | | function updatePaginationState(response) { |
| | | pagination.total = Number(response?.total || 0) |
| | | pagination.current = Number(response?.current || pagination.current || 1) |
| | | pagination.size = Number(response?.size || pagination.size || 20) |
| | | } |
| | | |
| | | async function loadRows() { |
| | | if (!props.visible || !props.taskRow?.taskCode) { |
| | | return |
| | | } |
| | | |
| | | loading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchFlowStepInstancePage({ |
| | | taskNo: props.taskRow.taskCode, |
| | | current: pagination.current, |
| | | pageSize: pagination.size |
| | | }), |
| | | { |
| | | records: [], |
| | | total: 0, |
| | | current: pagination.current, |
| | | size: pagination.size |
| | | }, |
| | | { timeoutMessage: '流程步骤加载超时,已停止等待' } |
| | | ) |
| | | rows.value = Array.isArray(response?.records) |
| | | ? response.records.map((record) => normalizeFlowStepInstanceRow(record)) |
| | | : [] |
| | | updatePaginationState(response) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleSizeChange(size) { |
| | | pagination.size = size |
| | | pagination.current = 1 |
| | | void loadRows() |
| | | } |
| | | |
| | | function handleCurrentChange(current) { |
| | | pagination.current = current |
| | | void loadRows() |
| | | } |
| | | |
| | | watch( |
| | | () => [props.visible, props.taskRow?.taskCode], |
| | | ([visible]) => { |
| | | if (!visible) { |
| | | rows.value = [] |
| | | pagination.current = 1 |
| | | return |
| | | } |
| | | pagination.current = 1 |
| | | void loadRows() |
| | | } |
| | | ) |
| | | </script> |
| | |
| | | statusText: record['status$'] || '-', |
| | | updateTimeText: record['updateTime$'] || record.updateTime || '-', |
| | | createTimeText: record['createTime$'] || record.createTime || '-', |
| | | canComplete: record.canComplete === true |
| | | canComplete: record.canComplete === true, |
| | | canCancel: record.canCancel === true |
| | | } |
| | | } |
| | | |
| | |
| | | return Number(row.taskStatus) === 199 && Number(row.taskType) === 103 |
| | | } |
| | | |
| | | export function canTopTask(row = {}) { |
| | | const taskStatus = Number(row.taskStatus) |
| | | const taskType = Number(row.taskType) |
| | | const allowedStatuses = [1, 101] |
| | | const allowedTypes = [1, 101, 10, 103, 11] |
| | | return allowedStatuses.includes(taskStatus) && allowedTypes.includes(taskType) |
| | | } |
| | | |
| | | export function getTaskActionList(row = {}) { |
| | | return [ |
| | | { |
| | | key: 'view', |
| | | label: '查看详情', |
| | | icon: 'ri:eye-line' |
| | | }, |
| | | { |
| | | key: 'flowStep', |
| | | label: '流程步骤', |
| | | icon: 'ri:node-tree' |
| | | }, |
| | | ...(row.canComplete |
| | | ? [ |
| | |
| | | } |
| | | ] |
| | | : []), |
| | | ...(canTopTask(row) |
| | | ? [ |
| | | { |
| | | key: 'top', |
| | | label: '任务置顶', |
| | | icon: 'ri:pushpin-line', |
| | | auth: 'update' |
| | | }, |
| | | } |
| | | ] |
| | | : []), |
| | | ...(row.canCancel |
| | | ? [ |
| | | { |
| | | key: 'remove', |
| | | label: '取消任务', |
| | |
| | | auth: 'delete' |
| | | } |
| | | ] |
| | | : []) |
| | | ] |
| | | } |
| | | |
| | | export async function confirmTaskAction(message) { |
| | |
| | | import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue' |
| | | import { getTaskActionList } from './taskPage.helpers' |
| | | |
| | | export function createTaskTableColumns(handleActionClick) { |
| | | export function createTaskTableColumns({ handleActionClick, createExpandContent }) { |
| | | return [ |
| | | ...(createExpandContent |
| | | ? [ |
| | | { |
| | | type: 'expand', |
| | | width: 56, |
| | | formatter: (row) => createExpandContent(row) |
| | | } |
| | | ] |
| | | : []), |
| | | { |
| | | prop: 'taskCode', |
| | | label: '任务号', |
| | |
| | | export function createAsnOrderItemSearchState() { |
| | | return { |
| | | condition: '', |
| | | orderId: '', |
| | | orderCode: '', |
| | | poCode: '', |
| | | platWorkCode: '', |
| | |
| | | |
| | | ;[ |
| | | 'condition', |
| | | 'orderId', |
| | | 'orderCode', |
| | | 'poCode', |
| | | 'platWorkCode', |
| | |
| | | <template> |
| | | <div class="asn-order-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>入库通知单ID:{{ activeSourceSummary.orderId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | |
| | | const DEFAULT_PAGE_SIZE = 20 |
| | | |
| | | const userStore = useUserStore() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const reportTitle = ASN_ORDER_ITEM_REPORT_TITLE |
| | | const searchForm = ref(createAsnOrderItemSearchState()) |
| | | const selectedRows = ref([]) |
| | |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | const activeItemId = ref(null) |
| | | |
| | | const activeSourceSummary = computed(() => { |
| | | if (searchForm.value.orderId === '' || searchForm.value.orderId === undefined || searchForm.value.orderId === null) { |
| | | return null |
| | | } |
| | | return { |
| | | orderId: searchForm.value.orderId |
| | | } |
| | | }) |
| | | |
| | | const reportQueryParams = computed(() => ({ |
| | | ...buildAsnOrderItemSearchParams(searchForm.value), |
| | |
| | | resetSearchParams() |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const orderId = route.query.orderId |
| | | if (orderId === undefined || orderId === null || orderId === '') { |
| | | return |
| | | } |
| | | searchForm.value.orderId = String(orderId) |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.orderId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | orderId: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams( |
| | | buildAsnOrderItemPageQueryParams({ |
| | | ...searchForm.value, |
| | | orderBy: 'id desc' |
| | | }) |
| | | ) |
| | | getData() |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | activeItemId.value = row.id |
| | | detailData.value = normalizeAsnOrderItemDetail(row) |
| | |
| | | ASN_ORDER_ITEM_REPORT_STYLE.orientation |
| | | }) |
| | | ) |
| | | |
| | | watch( |
| | | () => route.query.orderId, |
| | | (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams( |
| | | buildAsnOrderItemPageQueryParams({ |
| | | ...searchForm.value, |
| | | orderBy: 'id desc' |
| | | }) |
| | | ) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams( |
| | | buildAsnOrderItemPageQueryParams({ |
| | | ...searchForm.value, |
| | | orderBy: 'id desc' |
| | | }) |
| | | ) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | icon: 'ri:eye-line' |
| | | }, |
| | | { |
| | | key: 'items', |
| | | label: '收货明细', |
| | | icon: 'ri:list-check-3' |
| | | }, |
| | | { |
| | | key: 'print', |
| | | label: '打印', |
| | | icon: 'ri:printer-line' |
| | |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | |
| | | fetchAsnOrderPage, |
| | | fetchCompleteAsnOrder, |
| | | fetchExportAsnOrderReport, |
| | | fetchGetAsnOrderDetail, |
| | | fetchGetAsnOrderMany |
| | | } from '@/api/asn-order' |
| | | import AsnOrderDetailDrawer from './modules/asn-order-detail-drawer.vue' |
| | |
| | | defineOptions({ name: 'AsnOrder' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const reportTitle = ASN_ORDER_REPORT_TITLE |
| | | const searchForm = ref(createAsnOrderSearchState()) |
| | | const selectedRows = ref([]) |
| | |
| | | const detailData = ref({}) |
| | | const detailTableData = ref([]) |
| | | const activeOrderId = ref(null) |
| | | const activeOrderRow = ref(null) |
| | | const poDialogVisible = ref(false) |
| | | |
| | | const detailPagination = reactive({ |
| | |
| | | |
| | | async function openDetail(row) { |
| | | activeOrderId.value = row.id |
| | | activeOrderRow.value = row |
| | | detailData.value = normalizeAsnOrderRow(row) |
| | | detailPagination.current = 1 |
| | | detailDrawerVisible.value = true |
| | | await loadDetailResources() |
| | |
| | | |
| | | if (action.key === 'print') { |
| | | await handlePrint({ ids: [row.id], pageSize: 1 }) |
| | | return |
| | | } |
| | | |
| | | if (action.key === 'items') { |
| | | router.push({ |
| | | path: '/orders/asn-order-item', |
| | | query: { |
| | | orderId: String(row.id) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | |
| | |
| | | |
| | | detailLoading.value = true |
| | | try { |
| | | const [detailResponse, itemResponse] = await Promise.all([ |
| | | guardRequestWithMessage( |
| | | fetchGetAsnOrderDetail(activeOrderId.value), |
| | | {}, |
| | | { |
| | | timeoutMessage: '入库通知单详情加载超时,已停止等待' |
| | | } |
| | | ), |
| | | guardRequestWithMessage( |
| | | const itemResponse = await guardRequestWithMessage( |
| | | fetchAsnOrderItemPage( |
| | | buildAsnOrderDetailQueryParams({ |
| | | orderId: activeOrderId.value, |
| | |
| | | timeoutMessage: '入库通知单明细加载超时,已停止等待' |
| | | } |
| | | ) |
| | | ]) |
| | | |
| | | detailData.value = normalizeAsnOrderRow(detailResponse) |
| | | detailData.value = normalizeAsnOrderRow(activeOrderRow.value || {}) |
| | | detailTableData.value = Array.isArray(itemResponse?.records) |
| | | ? itemResponse.records.map((item) => normalizeAsnOrderItemRow(item)) |
| | | : [] |
| | |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="入库通知单详情" |
| | | size="88%" |
| | | size="1180px" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | > |
| | | <div class="flex h-full flex-col gap-4"> |
| | | <ElDescriptions :column="4" border> |
| | | <ElScrollbar class="h-[calc(100vh-180px)] pr-1"> |
| | | <div class="space-y-4"> |
| | | <ElDescriptions title="基础信息" :column="4" border> |
| | | <ElDescriptionsItem label="ASN单号">{{ detail.code || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="PO单号">{{ detail.poCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="业务类型">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据类型">{{ |
| | | detail.orderTypeLabel || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据状态">{{ |
| | | detail.exceStatusText || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="采购组织">{{ |
| | | detail.purchaseOrgName || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="采购员">{{ |
| | | detail.purchaseUserName || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据类型">{{ detail.orderTypeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据状态">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="采购组织">{{ detail.purchaseOrgName || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="采购员">{{ detail.purchaseUserName || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="供应商">{{ detail.supplierName || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="应收数量">{{ detail.anfme ?? 0 }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="已收数量">{{ detail.qty ?? 0 }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ |
| | | detail.updateTimeText || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ |
| | | detail.createTimeText || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="备注" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | |
| | | <div class="space-y-3"> |
| | | <div class="flex items-center justify-between"> |
| | | <div class="text-sm text-[var(--art-gray-600)]" |
| | | >明细清单(物料编码/物料名称/供应商批次)</div |
| | | > |
| | | <div class="text-sm font-medium text-[var(--art-gray-900)]">单据明细</div> |
| | | <div class="flex items-center gap-3"> |
| | | <ElTag effect="plain">共 {{ data.length }} 条</ElTag> |
| | | <ElButton :loading="loading" @click="$emit('refresh')">刷新</ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <ArtTable |
| | |
| | | @pagination:current-change="$emit('current-change', $event)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | |
| | | export function createDeliveryItemSearchState() { |
| | | return { |
| | | condition: '', |
| | | deliveryId: '', |
| | | deliveryCode: '', |
| | | platItemId: '', |
| | | matnrCode: '', |
| | |
| | | <template> |
| | | <div class="delivery-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>DO单ID:{{ activeSourceSummary.deliveryId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchDeliveryItemPage, fetchGetDeliveryItemDetail } from '@/api/delivery' |
| | |
| | | |
| | | defineOptions({ name: 'DeliveryItem' }) |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const searchForm = ref(createDeliveryItemSearchState()) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | |
| | | const activeSourceSummary = computed(() => { |
| | | if (searchForm.value.deliveryId === '' || searchForm.value.deliveryId === undefined || searchForm.value.deliveryId === null) { |
| | | return null |
| | | } |
| | | return { |
| | | deliveryId: searchForm.value.deliveryId |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | Object.assign(searchForm.value, createDeliveryItemSearchState()) |
| | | resetSearchParams() |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const deliveryId = route.query.deliveryId |
| | | if (deliveryId === undefined || deliveryId === null || deliveryId === '') { |
| | | return |
| | | } |
| | | searchForm.value.deliveryId = Number.isFinite(Number(deliveryId)) |
| | | ? Number(deliveryId) |
| | | : searchForm.value.deliveryId |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.deliveryId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | deliveryId: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | watch( |
| | | () => route.query.deliveryId, |
| | | (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | key: 'view', |
| | | label: '查看详情', |
| | | icon: 'ri:eye-line' |
| | | }, |
| | | { |
| | | key: 'items', |
| | | label: '明细', |
| | | icon: 'ri:list-check-3' |
| | | } |
| | | ] |
| | | |
| | |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | |
| | | defineOptions({ name: 'Delivery' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const reportTitle = 'DO单报表' |
| | | const searchForm = ref(createDeliverySearchState()) |
| | | const selectedRows = ref([]) |
| | |
| | | openDetail(row) |
| | | return |
| | | } |
| | | if (action.key === 'items') { |
| | | if (!row?.id) { |
| | | return |
| | | } |
| | | router.push({ |
| | | path: '/orders/delivery-item', |
| | | query: { |
| | | deliveryId: String(row.id) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | if (action.key === 'delete') { |
| | | handleDelete(row) |
| | | } |
| | |
| | | <template> |
| | | <div class="out-stock-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>出库单ID:{{ activeSourceSummary.orderId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchGetOutStockItemDetail, fetchOutStockItemPage } from '@/api/out-stock-item' |
| | |
| | | |
| | | defineOptions({ name: 'OutStockItem' }) |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const searchForm = ref(createOutStockItemSearchState()) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | const activeSourceSummary = computed(() => { |
| | | if ( |
| | | searchForm.value.orderId === '' || |
| | | searchForm.value.orderId === undefined || |
| | | searchForm.value.orderId === null |
| | | ) { |
| | | return null |
| | | } |
| | | return { |
| | | orderId: searchForm.value.orderId |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | Object.assign(searchForm.value, createOutStockItemSearchState()) |
| | | resetSearchParams() |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const orderId = route.query.orderId |
| | | if (orderId === undefined || orderId === null || orderId === '') { |
| | | return |
| | | } |
| | | searchForm.value.orderId = Number.isFinite(Number(orderId)) |
| | | ? Number(orderId) |
| | | : searchForm.value.orderId |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.orderId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | orderId: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams(buildOutStockItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | watch( |
| | | () => route.query.orderId, |
| | | (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildOutStockItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildOutStockItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | export function createOutStockItemSearchState() { |
| | | return { |
| | | condition: '', |
| | | orderId: '', |
| | | orderCode: '', |
| | | poCode: '', |
| | | platItemId: '', |
| | |
| | | ].forEach((key) => pushText(result, key, params[key])) |
| | | |
| | | pushNumber(result, 'status', params.status) |
| | | pushNumber(result, 'orderId', params.orderId) |
| | | |
| | | return result |
| | | } |
| | |
| | | /> |
| | | </ElCard> |
| | | |
| | | <OutStockDetailDrawer v-model:visible="detailDrawerVisible" :loading="detailLoading" :detail="detailData" /> |
| | | <OutStockDetailDrawer |
| | | v-model:visible="detailDrawerVisible" |
| | | :loading="detailLoading" |
| | | :items-loading="detailItemsLoading" |
| | | :detail="detailData" |
| | | :item-rows="detailItemRows" |
| | | :item-columns="detailItemColumns" |
| | | :pagination="detailItemPagination" |
| | | @refresh="loadDetailResources" |
| | | @size-change="handleDetailSizeChange" |
| | | @current-change="handleDetailCurrentChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | | import ListExportPrint from '@/components/biz/list-export-print/index.vue' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchOutStockItemPage } from '@/api/out-stock-item' |
| | | import { |
| | | fetchCancelOutStock, |
| | | fetchCompleteOutStock, |
| | |
| | | normalizeOutStockRow |
| | | } from './outStockPage.helpers' |
| | | import { createOutStockTableColumns } from './outStockTable.columns' |
| | | import { createOutStockItemTableColumns } from '../out-stock-item/outStockItemTable.columns.js' |
| | | import { normalizeOutStockItemRow } from '../out-stock-item/outStockItemPage.helpers.js' |
| | | |
| | | defineOptions({ name: 'OutStock' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const reportTitle = OUT_STOCK_REPORT_TITLE |
| | | const searchForm = ref(createOutStockSearchState()) |
| | | const selectedRows = ref([]) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailItemsLoading = ref(false) |
| | | const detailData = ref({}) |
| | | const detailItemRows = ref([]) |
| | | const activeOutStockId = ref(null) |
| | | const detailItemPagination = reactive({ |
| | | current: 1, |
| | | size: 20, |
| | | total: 0 |
| | | }) |
| | | const detailItemColumns = computed(() => |
| | | createOutStockItemTableColumns().filter((column) => column.prop !== 'operation') |
| | | ) |
| | | |
| | | const reportQueryParams = computed(() => buildOutStockSearchParams(searchForm.value)) |
| | | const searchItems = computed(() => [ |
| | |
| | | { label: '备注', key: 'memo', type: 'input', props: { clearable: true, placeholder: '请输入备注' } } |
| | | ]) |
| | | |
| | | function openDetail(row) { |
| | | async function loadDetailResources() { |
| | | if (!activeOutStockId.value) { |
| | | return |
| | | } |
| | | |
| | | detailLoading.value = true |
| | | detailItemsLoading.value = true |
| | | try { |
| | | const [detailResponse, itemResponse] = await Promise.all([ |
| | | guardRequestWithMessage(fetchGetOutStockDetail(activeOutStockId.value), {}, { timeoutMessage: '出库单详情加载超时,已停止等待' }), |
| | | guardRequestWithMessage( |
| | | fetchOutStockItemPage({ |
| | | orderId: activeOutStockId.value, |
| | | current: detailItemPagination.current, |
| | | pageSize: detailItemPagination.size |
| | | }), |
| | | { |
| | | records: [], |
| | | total: 0, |
| | | current: detailItemPagination.current, |
| | | size: detailItemPagination.size |
| | | }, |
| | | { timeoutMessage: '出库单明细加载超时,已停止等待' } |
| | | ) |
| | | ]) |
| | | |
| | | detailData.value = normalizeOutStockRow(detailResponse) |
| | | const normalized = defaultResponseAdapter(itemResponse) |
| | | detailItemRows.value = normalized.records.map((item) => normalizeOutStockItemRow(item)) |
| | | detailItemPagination.total = Number(normalized.total || 0) |
| | | detailItemPagination.current = Number(normalized.current || detailItemPagination.current || 1) |
| | | detailItemPagination.size = Number(normalized.size || detailItemPagination.size || 20) |
| | | } finally { |
| | | detailLoading.value = false |
| | | detailItemsLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | activeOutStockId.value = row.id |
| | | detailDrawerVisible.value = true |
| | | detailLoading.value = true |
| | | guardRequestWithMessage(fetchGetOutStockDetail(row.id), {}, { timeoutMessage: '出库单详情加载超时,已停止等待' }) |
| | | .then((detail) => { |
| | | detailData.value = normalizeOutStockRow(detail) |
| | | }) |
| | | .catch((error) => { |
| | | detailItemPagination.current = 1 |
| | | detailData.value = {} |
| | | detailItemRows.value = [] |
| | | try { |
| | | await loadDetailResources() |
| | | } catch (error) { |
| | | detailDrawerVisible.value = false |
| | | detailData.value = {} |
| | | detailItemRows.value = [] |
| | | ElMessage.error(error?.message || '获取出库单详情失败') |
| | | }) |
| | | .finally(() => { |
| | | detailLoading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | function handleDetailSizeChange(size) { |
| | | detailItemPagination.size = size |
| | | detailItemPagination.current = 1 |
| | | loadDetailResources() |
| | | } |
| | | |
| | | function handleDetailCurrentChange(current) { |
| | | detailItemPagination.current = current |
| | | loadDetailResources() |
| | | } |
| | | |
| | | async function handleComplete(row) { |
| | |
| | | ElMessage.success('完成成功') |
| | | await refreshData() |
| | | if (detailDrawerVisible.value && activeOutStockId.value === row.id) { |
| | | openDetail(row) |
| | | await loadDetailResources() |
| | | } |
| | | } catch (error) { |
| | | if (error === 'cancel' || error?.message === 'cancel') return |
| | |
| | | ElMessage.success('取消成功') |
| | | await refreshData() |
| | | if (detailDrawerVisible.value && activeOutStockId.value === row.id) { |
| | | openDetail(row) |
| | | await loadDetailResources() |
| | | } |
| | | } catch (error) { |
| | | if (error === 'cancel' || error?.message === 'cancel') return |
| | |
| | | openDetail(row) |
| | | return |
| | | } |
| | | if (action.key === 'items') { |
| | | router.push({ |
| | | path: '/orders/out-stock-item', |
| | | query: { |
| | | orderId: String(row.id) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | if (action.key === 'print') { |
| | | await handlePrint({ ids: [row.id], pageSize: 1 }) |
| | | return |
| | |
| | | <ElDrawer |
| | | v-model="visibleProxy" |
| | | :title="'出库单详情'" |
| | | size="720px" |
| | | size="1180px" |
| | | destroy-on-close |
| | | append-to-body |
| | | > |
| | | <ElScrollbar class="h-full"> |
| | | <ElScrollbar class="h-[calc(100vh-180px)] pr-1"> |
| | | <ElSkeleton :loading="loading" animated :rows="8"> |
| | | <div class="space-y-4"> |
| | | <ElDescriptions :column="2" border> |
| | | <ElDescriptions title="基础信息" :column="2" border> |
| | | <ElDescriptionsItem label="出库单号">{{ detail.code || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="PO单号">{{ detail.poCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据类型">{{ detail.typeLabel || '--' }}</ElDescriptionsItem> |
| | |
| | | <ElDescriptionsItem label="备注" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | |
| | | <ElDescriptions :column="2" border> |
| | | <ElDescriptions title="审计信息" :column="2" border> |
| | | <ElDescriptionsItem label="创建人">{{ detail.createByText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="修改人">{{ detail.updateByText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="修改时间">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | |
| | | <div class="space-y-3"> |
| | | <div class="flex items-center justify-between"> |
| | | <div class="text-sm font-medium text-[var(--art-gray-900)]">单据明细</div> |
| | | <div class="flex items-center gap-3"> |
| | | <ElTag effect="plain">共 {{ itemRows.length }} 条</ElTag> |
| | | <ElButton :loading="itemsLoading" @click="$emit('refresh')">刷新</ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <ArtTable |
| | | :loading="itemsLoading" |
| | | :data="itemRows" |
| | | :columns="itemColumns" |
| | | :pagination="pagination" |
| | | @pagination:size-change="$emit('size-change', $event)" |
| | | @pagination:current-change="$emit('current-change', $event)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ElSkeleton> |
| | | </ElScrollbar> |
| | |
| | | |
| | | <script setup> |
| | | import { computed } from 'vue' |
| | | import ArtTable from '@/components/core/tables/art-table/index.vue' |
| | | |
| | | defineOptions({ name: 'OutStockDetailDrawer' }) |
| | | |
| | |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | itemsLoading: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | detail: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | itemRows: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | itemColumns: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | pagination: { |
| | | type: Object, |
| | | default: () => ({ current: 1, size: 20, total: 0 }) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible']) |
| | | const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change']) |
| | | |
| | | const visibleProxy = computed({ |
| | | get: () => props.visible, |
| | |
| | | icon: 'ri:eye-line' |
| | | }, |
| | | { |
| | | key: 'items', |
| | | label: '明细', |
| | | icon: 'ri:list-check-3' |
| | | }, |
| | | { |
| | | key: 'print', |
| | | label: '打印', |
| | | icon: 'ri:printer-line' |
| | |
| | | <template> |
| | | <div class="preparation-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>备料单ID:{{ activeSourceSummary.orderId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | |
| | | defineOptions({ name: 'PreparationItem' }) |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const userStore = useUserStore() |
| | | const initialOrderId = route.query.orderId || route.query.id |
| | | const searchForm = ref( |
| | |
| | | const reportTitle = PREPARATION_ITEM_REPORT_TITLE |
| | | const reportColumns = getPreparationItemReportColumns() |
| | | const reportQueryParams = computed(() => buildPreparationItemSearchParams(searchForm.value)) |
| | | const activeSourceSummary = computed(() => { |
| | | if ( |
| | | searchForm.value.orderId === '' || |
| | | searchForm.value.orderId === undefined || |
| | | searchForm.value.orderId === null |
| | | ) { |
| | | return null |
| | | } |
| | | return { |
| | | orderId: searchForm.value.orderId |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | resetSearchParams(buildPreparationItemPageQueryParams(createPreparationItemSearchState(resetSeed))) |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const orderId = route.query.orderId || route.query.id |
| | | if (orderId === undefined || orderId === null || orderId === '') { |
| | | return |
| | | } |
| | | searchForm.value.orderId = Number.isFinite(Number(orderId)) |
| | | ? Number(orderId) |
| | | : searchForm.value.orderId |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.orderId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | orderId: undefined, |
| | | id: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams(buildPreparationItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | watch( |
| | | () => [route.query.orderId, route.query.id], |
| | | ([orderId, id]) => { |
| | | if ( |
| | | (orderId === undefined || orderId === null || orderId === '') && |
| | | (id === undefined || id === null || id === '') |
| | | ) { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildPreparationItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | const resolvePrintRecords = async (payload) => { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchGetPreparationItemMany(payload.ids)).records |
| | |
| | | previewMeta.value?.reportStyle?.orientation || PREPARATION_ITEM_REPORT_STYLE.orientation |
| | | }) |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildPreparationItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | <script setup> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | |
| | | defineOptions({ name: 'Preparation' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const reportTitle = PREPARATION_REPORT_TITLE |
| | | const searchForm = ref(createPreparationSearchState()) |
| | | const selectedRows = ref([]) |
| | |
| | | return |
| | | } |
| | | |
| | | if (action.key === 'items') { |
| | | router.push({ |
| | | path: '/orders/preparation-item', |
| | | query: { |
| | | orderId: String(row.id) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | |
| | | if (action.key === 'complete') { |
| | | await ElMessageBox.confirm(`确定完成备料单 ${row.code || ''} 吗?`, '完成确认', { |
| | | confirmButtonText: '确定', |
| | |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="备料单详情" |
| | | size="88%" |
| | | size="1180px" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | > |
| | | <div class="flex h-full flex-col gap-4"> |
| | | <ElDescriptions :column="4" border> |
| | | <ElScrollbar class="h-[calc(100vh-180px)] pr-1"> |
| | | <div class="space-y-4"> |
| | | <ElDescriptions title="基础信息" :column="4" border> |
| | | <ElDescriptionsItem label="备料单号">{{ detail.code || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="PO单号">{{ detail.poCode || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="业务类型">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据类型">{{ detail.typeLabel || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="单据状态"> |
| | | <ElTag :type="detail.exceStatusTagType || 'info'">{{ detail.exceStatusText || '--' }}</ElTag> |
| | | <ElTag :type="detail.exceStatusTagType || 'info'" effect="light"> |
| | | {{ detail.exceStatusText || '--' }} |
| | | </ElTag> |
| | | </ElDescriptionsItem> |
| | | <ElDescriptionsItem label="释放状态"> |
| | | <ElTag :type="detail.rleStatusTagType || 'info'">{{ detail.rleStatusText || '--' }}</ElTag> |
| | | <ElTag :type="detail.rleStatusTagType || 'info'" effect="light"> |
| | | {{ detail.rleStatusText || '--' }} |
| | | </ElTag> |
| | | </ElDescriptionsItem> |
| | | <ElDescriptionsItem label="物流单号">{{ detail.logisNo || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="业务时间">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem> |
| | |
| | | <ElDescriptionsItem label="备注" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | |
| | | <div class="space-y-3"> |
| | | <div class="flex items-center justify-between"> |
| | | <div class="text-sm text-[var(--art-gray-600)]">明细清单(物料编码/物料名称/供应商批次)</div> |
| | | <div class="text-sm font-medium text-[var(--art-gray-900)]">单据明细</div> |
| | | <div class="flex items-center gap-3"> |
| | | <ElTag effect="plain">共 {{ data.length }} 条</ElTag> |
| | | <ElButton :loading="loading" @click="$emit('refresh')">刷新</ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <ArtTable |
| | |
| | | @pagination:current-change="$emit('current-change', $event)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | |
| | | const normalizedRow = normalizePreparationRow(row) |
| | | return [ |
| | | { key: 'view', label: '查看详情', icon: 'ri:eye-line' }, |
| | | { key: 'items', label: '明细', icon: 'ri:list-check-3' }, |
| | | { key: 'print', label: '打印', icon: 'ri:printer-line' }, |
| | | { |
| | | key: 'complete', |
| | |
| | | <template> |
| | | <div class="purchase-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>PO单ID:{{ activeSourceSummary.purchaseId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | |
| | | defineOptions({ name: 'PurchaseItem' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const reportTitle = PURCHASE_ITEM_REPORT_TITLE |
| | | const searchForm = ref(createPurchaseItemSearchState()) |
| | | const selectedRows = ref([]) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | |
| | | const activeSourceSummary = computed(() => { |
| | | if (searchForm.value.purchaseId === '' || searchForm.value.purchaseId === undefined || searchForm.value.purchaseId === null) { |
| | | return null |
| | | } |
| | | return { |
| | | purchaseId: searchForm.value.purchaseId |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | resetSearchParams() |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const purchaseId = route.query.purchaseId |
| | | if (purchaseId === undefined || purchaseId === null || purchaseId === '') { |
| | | return |
| | | } |
| | | searchForm.value.purchaseId = Number.isFinite(Number(purchaseId)) |
| | | ? Number(purchaseId) |
| | | : searchForm.value.purchaseId |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.purchaseId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | purchaseId: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams(buildPurchaseItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | const buildPreviewMeta = (rows) => { |
| | | const now = new Date() |
| | | return { |
| | |
| | | }) |
| | | ) |
| | | |
| | | watch( |
| | | () => route.query.purchaseId, |
| | | (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildPurchaseItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildPurchaseItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | |
| | | |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | |
| | | const reportTitle = PURCHASE_REPORT_TITLE |
| | | const searchForm = ref(createPurchaseSearchState()) |
| | |
| | | } |
| | | } |
| | | |
| | | function openPurchaseItems(row) { |
| | | if (!row?.id) { |
| | | return |
| | | } |
| | | router.push({ |
| | | path: '/orders/purchase-item', |
| | | query: { |
| | | purchaseId: String(row.id) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const { |
| | | columns, |
| | | columnChecks, |
| | |
| | | columnsFactory: () => |
| | | createPurchaseTableColumns({ |
| | | handleView: openDetail, |
| | | handleViewItems: openPurchaseItems, |
| | | handleEdit: hasAuth('update') ? openEditDialog : null, |
| | | handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null, |
| | | canEdit: hasAuth('update'), |
| | |
| | | |
| | | export function createPurchaseTableColumns({ |
| | | handleView, |
| | | handleViewItems, |
| | | handleEdit, |
| | | handleDelete, |
| | | canEdit = true, |
| | |
| | | formatter: (row) => { |
| | | const operations = [{ key: 'view', label: '详情', icon: 'ri:eye-line' }] |
| | | |
| | | if (handleViewItems) { |
| | | operations.push({ |
| | | key: 'items', |
| | | label: '明细', |
| | | icon: 'ri:list-check-3' |
| | | }) |
| | | } |
| | | |
| | | if (canEdit && handleEdit) { |
| | | operations.push({ key: 'edit', label: '编辑', icon: 'ri:pencil-line' }) |
| | | } |
| | |
| | | list: operations, |
| | | onClick: (item) => { |
| | | if (item.key === 'view') handleView?.(row) |
| | | if (item.key === 'items') handleViewItems?.(row) |
| | | if (item.key === 'edit') handleEdit?.(row) |
| | | if (item.key === 'delete') handleDelete?.(row) |
| | | } |
| | |
| | | <template> |
| | | <div class="transfer-item-page art-full-height"> |
| | | <ElCard v-if="activeSourceSummary" class="mb-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]"> |
| | | <span class="font-medium text-[var(--art-text-gray-900)]">当前来源</span> |
| | | <span>调拨单ID:{{ activeSourceSummary.transferId }}</span> |
| | | </div> |
| | | <ElButton link type="primary" @click="handleClearSourceFilter">查看全部</ElButton> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { ElButton, ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | |
| | | defineOptions({ name: 'TransferItem' }) |
| | | |
| | | const userStore = useUserStore() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const reportTitle = TRANSFER_ITEM_REPORT_TITLE |
| | | const searchForm = ref(createTransferItemSearchState()) |
| | | const selectedRows = ref([]) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | |
| | | const activeSourceSummary = computed(() => { |
| | | if (searchForm.value.transferId === '' || searchForm.value.transferId === undefined || searchForm.value.transferId === null) { |
| | | return null |
| | | } |
| | | return { |
| | | transferId: searchForm.value.transferId |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | resetSearchParams() |
| | | } |
| | | |
| | | function applyRouteSearch() { |
| | | const transferId = route.query.transferId |
| | | if (transferId === undefined || transferId === null || transferId === '') { |
| | | return |
| | | } |
| | | searchForm.value.transferId = Number.isFinite(Number(transferId)) |
| | | ? Number(transferId) |
| | | : searchForm.value.transferId |
| | | } |
| | | |
| | | function handleClearSourceFilter() { |
| | | searchForm.value.transferId = '' |
| | | router.replace({ |
| | | path: route.path, |
| | | query: { |
| | | ...route.query, |
| | | transferId: undefined |
| | | } |
| | | }) |
| | | replaceSearchParams(buildTransferItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | const resolvePrintRecords = async (payload) => { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchTransferItemMany(payload.ids)).records |
| | |
| | | orientation: previewMeta.value?.reportStyle?.orientation || TRANSFER_ITEM_REPORT_STYLE.orientation |
| | | }) |
| | | ) |
| | | </script> |
| | | |
| | | watch( |
| | | () => route.query.transferId, |
| | | (value) => { |
| | | if (value === undefined || value === null || value === '') { |
| | | return |
| | | } |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildTransferItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | applyRouteSearch() |
| | | replaceSearchParams(buildTransferItemPageQueryParams(searchForm.value)) |
| | | getData() |
| | | }) |
| | | </script> |
| | |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | | import { useUserStore } from '@/store/modules/user' |
| | |
| | | |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | |
| | | const reportTitle = TRANSFER_REPORT_TITLE |
| | | const searchForm = ref(createTransferSearchState()) |
| | |
| | | await openDetail(row) |
| | | return |
| | | } |
| | | if (action?.key === 'items') { |
| | | if (!row?.id) return |
| | | router.push({ |
| | | path: '/orders/transfer-item', |
| | | query: { |
| | | transferId: String(row.id) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | if (action?.key === 'edit') { |
| | | if (!canUpdate.value) return |
| | | await openEditDialog(row) |
| | |
| | | const normalizedRow = normalizeTransferRow(row) |
| | | const actions = [ |
| | | { key: 'view', label: '查看详情', icon: 'ri:eye-line' }, |
| | | { key: 'items', label: '明细', icon: 'ri:list-check-3' }, |
| | | { key: 'edit', label: '编辑', icon: 'ri:pencil-line' } |
| | | ] |
| | | if (Number(normalizedRow.exceStatus) === 0) { |
| | |
| | | } |
| | | }, |
| | | { |
| | | prop: 'component', |
| | | label: '组件标识', |
| | | minWidth: 160, |
| | | showOverflowTooltip: true, |
| | | formatter: (row) => { |
| | | if (row.meta?.isAuthButton) return '' |
| | | return row.component || '' |
| | | } |
| | | }, |
| | | { |
| | | prop: 'authority', |
| | | label: '权限标识', |
| | | minWidth: 180, |
| | |
| | | prop: 'sort', |
| | | label: '排序', |
| | | width: 90 |
| | | }, |
| | | { |
| | | prop: 'id', |
| | | label: 'ID', |
| | | width: 96, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | prop: 'status', |
| | |
| | | prop: 'operation', |
| | | label: '操作', |
| | | width: 180, |
| | | align: 'right', |
| | | align: 'center', |
| | | formatter: (row) => { |
| | | const buttonStyle = { class: 'flex justify-end' } |
| | | const buttonStyle = { class: 'flex justify-center' } |
| | | if (row.meta?.isAuthButton) { |
| | | return h('div', buttonStyle, [ |
| | | h(ArtButtonTable, { |
| | |
| | | writeTemplate("TableColumns", pageDirectory, simpleEntityName + "Table.columns.js"); |
| | | writeTemplate("Search", modulesDirectory, kebabEntityName + "-search.vue"); |
| | | writeTemplate("EditDialog", modulesDirectory, kebabEntityName + "-edit-dialog.vue"); |
| | | writeTemplate("Api", resolveFrontendApiDirectory(), normalizedFrontendApiModule + ".js"); |
| | | writeTemplate("Api", resolveFrontendApiDirectory(), resolveFrontendApiFileName()); |
| | | } |
| | | |
| | | private String resolveControllerDirectory() { |
| | |
| | | return directory; |
| | | } |
| | | return directory + normalizedFrontendApiModule.substring(0, index + 1); |
| | | } |
| | | |
| | | private String resolveFrontendApiFileName() { |
| | | int index = normalizedFrontendApiModule.lastIndexOf('/'); |
| | | if (index < 0) { |
| | | return normalizedFrontendApiModule + ".js"; |
| | | } |
| | | return normalizedFrontendApiModule.substring(index + 1) + ".js"; |
| | | } |
| | | |
| | | private void writeTemplate(String templateName, String directory, String fileName) throws IOException { |
| | |
| | | .append("StatusMeta(row.statusBool ?? row.status)),\n"); |
| | | continue; |
| | | } |
| | | if (isNumericColumn(column)) { |
| | | sb.append(" createNumberColumn('") |
| | | .append(column.getHumpName()) |
| | | .append("', '") |
| | | .append(escapeJs(resolveFieldLabel(column))) |
| | | .append("', 120),\n"); |
| | | continue; |
| | | } |
| | | if (isDisplayTextColumn(column)) { |
| | | sb.append(" createTextColumn('") |
| | | .append(column.getHumpName()) |
| | |
| | | .append("),\n"); |
| | | continue; |
| | | } |
| | | if (isNumericColumn(column)) { |
| | | sb.append(" createNumberColumn('") |
| | | .append(column.getHumpName()) |
| | | .append("', '") |
| | | .append(escapeJs(resolveFieldLabel(column))) |
| | | .append("', 120),\n"); |
| | | continue; |
| | | } |
| | | sb.append(" createTextColumn('") |
| | | .append(column.getHumpName()) |
| | | .append("', '") |
| | |
| | | .append(resolveTextColumnWidth(column)) |
| | | .append("),\n"); |
| | | } |
| | | return trimTrailingLineBreak(sb); |
| | | return trimTrailingLineBreakKeepComma(sb); |
| | | } |
| | | |
| | | private String buildExportRowContent() { |
| | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private String trimTrailingLineBreakKeepComma(StringBuilder sb) { |
| | | if (sb.length() == 0) { |
| | | return ""; |
| | | } |
| | | while (sb.length() > 0 && (sb.charAt(sb.length() - 1) == '\n' || sb.charAt(sb.length() - 1) == '\r')) { |
| | | sb.deleteCharAt(sb.length() - 1); |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private String safeText(String value) { |
| | | return value == null ? "" : value.trim(); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.common; |
| | | |
| | | import com.vincent.rsf.framework.generators.ReactGenerator; |
| | | import com.vincent.rsf.framework.generators.RsfDesignGenerator; |
| | | import com.vincent.rsf.framework.generators.constant.SqlOsType; |
| | | import com.vincent.rsf.framework.generators.domain.Column; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.io.TempDir; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.lang.reflect.Method; |
| | | import java.nio.charset.Charset; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertAll; |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | | class RsfDesignGeneratorRegressionTest { |
| | | |
| | | @TempDir |
| | | Path tempDir; |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsKeepsDisplayColumnsCommaAndNestedApiPath() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_test", |
| | | "系统测试表", |
| | | "test", |
| | | "Test", |
| | | "system/test", |
| | | "system/test", |
| | | buildColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | Path tableColumnsFile = tempDir.resolve("src/views/system/test/testTable.columns.js"); |
| | | Path pageHelpersFile = tempDir.resolve("src/views/system/test/testPage.helpers.js"); |
| | | Path apiFile = tempDir.resolve("src/api/system/test.js"); |
| | | |
| | | String tableColumnsContent = Files.readString(tableColumnsFile, StandardCharsets.UTF_8); |
| | | String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(Files.exists(apiFile), "嵌套 api 模块路径应生成到 src/api/system/test.js"), |
| | | () -> assertTrue(tableColumnsContent.contains("createTextColumn('levelText', '等级', 160),"), "数值枚举列应显示为 levelText"), |
| | | () -> assertTrue(tableColumnsContent.contains("createTextColumn('roleIdText', '角色', 160),"), "数值外键列应显示为 roleIdText"), |
| | | () -> assertFalse(tableColumnsContent.contains("createNumberColumn('level',"), "数值枚举列不应再走原始数字列"), |
| | | () -> assertFalse(tableColumnsContent.contains("createNumberColumn('roleId',"), "数值外键列不应再走原始数字列"), |
| | | () -> assertTrue( |
| | | tableColumnsContent.contains("createTextColumn('memo', '备注', 220),\r\n {") |
| | | || tableColumnsContent.contains("createTextColumn('memo', '备注', 220),\n {"), |
| | | "最后一个业务列和操作列之间应保留逗号" |
| | | ), |
| | | () -> assertTrue(pageHelpersContent.contains("{ source: 'levelText', label: '等级' }"), "报表列应包含 levelText"), |
| | | () -> assertTrue(pageHelpersContent.contains("{ source: 'roleIdText', label: '角色' }"), "报表列应包含 roleIdText") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsKeepsBooleanAndStatusTextMappingForPrintExport() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_switch", |
| | | "系统开关表", |
| | | "switchItem", |
| | | "SwitchItem", |
| | | "system/switch-item", |
| | | "system/switch-item", |
| | | buildBooleanColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | Path tableColumnsFile = tempDir.resolve("src/views/system/switch-item/switchItemTable.columns.js"); |
| | | Path pageHelpersFile = tempDir.resolve("src/views/system/switch-item/switchItemPage.helpers.js"); |
| | | |
| | | String tableColumnsContent = Files.readString(tableColumnsFile, StandardCharsets.UTF_8); |
| | | String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(tableColumnsContent.contains("createTextColumn('enabledText', '是否启用', 160),"), "布尔字段列表列应显示为 enabledText"), |
| | | () -> assertTrue(tableColumnsContent.contains("createTagColumn('status', '状态', 120"), "status 字段应继续走标签列"), |
| | | () -> assertTrue(pageHelpersContent.contains("enabledText: formatBooleanText(toOptionalBoolean(record.enabled))"), "布尔字段应生成 formatBooleanText 映射"), |
| | | () -> assertTrue(pageHelpersContent.contains("{ source: 'enabledText', label: '是否启用' }"), "布尔字段报表列应使用 enabledText"), |
| | | () -> assertTrue(pageHelpersContent.contains("{ source: 'statusText', label: '状态' }"), "status 报表列应使用 statusText"), |
| | | () -> assertTrue(pageHelpersContent.contains("status: 'statusText'"), "status 导出列别名应映射到 statusText") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsSupportsDeepNestedViewAndApiModules() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_deep_case", |
| | | "深层目录测试表", |
| | | "deepCase", |
| | | "DeepCase", |
| | | "basic-info/warehouse/deep-case", |
| | | "system/admin/deep-case", |
| | | buildColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/index.vue")), "深层 view 路径应生成 index.vue"), |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/deepCasePage.helpers.js")), "深层 view 路径应生成 helpers"), |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/deepCaseTable.columns.js")), "深层 view 路径应生成列文件"), |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/modules/deep-case-search.vue")), "深层 view 路径应生成 search 组件"), |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/modules/deep-case-edit-dialog.vue")), "深层 view 路径应生成 edit dialog"), |
| | | () -> assertTrue(Files.exists(tempDir.resolve("src/api/system/admin/deep-case.js")), "深层 api 模块路径应生成到正确目录") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsKeepsFormDefaultsPayloadAndRulesAligned() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_default_case", |
| | | "默认值测试表", |
| | | "defaultCase", |
| | | "DefaultCase", |
| | | "system/default-case", |
| | | "system/default-case", |
| | | buildDefaultCaseColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | Path pageHelpersFile = tempDir.resolve("src/views/system/default-case/defaultCasePage.helpers.js"); |
| | | Path editDialogFile = tempDir.resolve("src/views/system/default-case/modules/default-case-edit-dialog.vue"); |
| | | |
| | | String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); |
| | | String editDialogContent = Files.readString(editDialogFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(pageHelpersContent.contains("status: 1,"), "状态字段表单默认值应使用首个枚举值"), |
| | | () -> assertTrue(pageHelpersContent.contains("level: void 0,"), "数字字段表单默认值应为 void 0"), |
| | | () -> assertTrue(pageHelpersContent.contains("enabled: void 0,"), "布尔字段表单默认值应为 void 0"), |
| | | () -> assertTrue(pageHelpersContent.contains("status: hasValue(record.status) ? toOptionalNumber(record.status) : 1,"), "状态字段弹窗模型应保留默认回退值"), |
| | | () -> assertTrue(pageHelpersContent.contains("...buildNumberField('status', formData.status),"), "状态字段保存 payload 应走数字字段构造"), |
| | | () -> assertTrue(pageHelpersContent.contains("...(hasValue(formData.enabled) ? { enabled: toOptionalBoolean(formData.enabled) } : {}),"), "布尔字段保存 payload 应走布尔转换"), |
| | | () -> assertTrue(editDialogContent.contains("createInputFormItem('名称', 'name', '请输入名称'),"), "普通文本字段应生成输入框"), |
| | | () -> assertTrue(editDialogContent.contains("createInputFormItem('等级', 'level', '请输入等级', { type: 'number' }),"), "数字字段应生成 number 输入框"), |
| | | () -> assertTrue(editDialogContent.contains("createSelectFormItem('是否启用', 'enabled', '请选择是否启用', getDefaultCaseFieldOptions('enabled')),") , "布尔字段应生成下拉项"), |
| | | () -> assertTrue(editDialogContent.contains("createSelectFormItem('状态', 'status', '请选择状态', getDefaultCaseFieldOptions('status')),") , "枚举状态字段应生成下拉项"), |
| | | () -> assertTrue(editDialogContent.contains("name: [{ required: true, message: '请输入名称', trigger: 'blur' }],"), "文本必填规则应使用 blur/请输入"), |
| | | () -> assertTrue(editDialogContent.contains("status: [{ required: true, message: '请选择状态', trigger: 'change' }]"), "选择类必填规则应使用 change/请选择") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsKeepsSearchItemsAndQueryParamsAligned() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_search_case", |
| | | "搜索测试表", |
| | | "searchCase", |
| | | "SearchCase", |
| | | "system/search-case", |
| | | "system/search-case", |
| | | buildSearchCaseColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | Path pageHelpersFile = tempDir.resolve("src/views/system/search-case/searchCasePage.helpers.js"); |
| | | Path searchFile = tempDir.resolve("src/views/system/search-case/modules/search-case-search.vue"); |
| | | |
| | | String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); |
| | | String searchContent = Files.readString(searchFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(pageHelpersContent.contains("keyword: '',"), "文本搜索字段应出现在搜索状态中"), |
| | | () -> assertTrue(pageHelpersContent.contains("level: '',"), "数字搜索字段应出现在搜索状态中"), |
| | | () -> assertTrue(pageHelpersContent.contains("enabled: '',"), "布尔搜索字段应出现在搜索状态中"), |
| | | () -> assertTrue(pageHelpersContent.contains("status: ''"), "枚举搜索字段应出现在搜索状态中"), |
| | | () -> assertFalse(pageHelpersContent.contains("createTime: '',"), "日期字段不应进入搜索状态"), |
| | | () -> assertFalse(pageHelpersContent.contains("deleted: '',"), "托管字段不应进入搜索状态"), |
| | | () -> assertTrue(pageHelpersContent.contains("keyword: normalizeText(params.keyword),"), "文本搜索参数应走 normalizeText"), |
| | | () -> assertTrue(pageHelpersContent.contains("level: toOptionalNumber(params.level),"), "数字搜索参数应走 toOptionalNumber"), |
| | | () -> assertTrue(pageHelpersContent.contains("enabled: toOptionalBoolean(params.enabled),"), "布尔搜索参数应走 toOptionalBoolean"), |
| | | () -> assertTrue(pageHelpersContent.contains("status: toOptionalNumber(params.status)"), "枚举数字搜索参数应走 toOptionalNumber"), |
| | | () -> assertTrue(pageHelpersContent.contains("current: params.current || 1,"), "分页参数应统一 current 默认值"), |
| | | () -> assertTrue(pageHelpersContent.contains("pageSize: params.pageSize || params.size || 20,"), "分页参数应兼容 size/pageSize"), |
| | | () -> assertTrue(searchContent.contains("createInputSearchItem('关键字', 'condition', '请输入搜索测试表关键字'),"), "搜索栏应始终包含 condition 关键字搜索"), |
| | | () -> assertTrue(searchContent.contains("createInputSearchItem('关键字字段', 'keyword', '请输入关键字字段'),"), "文本搜索字段应生成输入框"), |
| | | () -> assertTrue(searchContent.contains("createInputSearchItem('等级', 'level', '请输入等级'),"), "数字搜索字段当前应生成输入框"), |
| | | () -> assertTrue(searchContent.contains("createSelectSearchItem('是否启用', 'enabled', '请选择是否启用', getSearchCaseFieldOptions('enabled')),") , "布尔搜索字段应生成下拉项"), |
| | | () -> assertTrue(searchContent.contains("createSelectSearchItem('状态', 'status', '请选择状态', getSearchCaseFieldOptions('status'))") , "枚举搜索字段应生成下拉项"), |
| | | () -> assertFalse(searchContent.contains("createTime"), "日期字段不应生成搜索项"), |
| | | () -> assertFalse(searchContent.contains("deleted"), "托管字段不应生成搜索项") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void writeControllerTemplateKeepsCrudAndExportContractAligned() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_controller_case", |
| | | "控制器测试表", |
| | | "controllerCase", |
| | | "ControllerCase", |
| | | "system/controller-case", |
| | | "system/controller-case", |
| | | buildControllerCaseColumns() |
| | | ); |
| | | generator.backendPrefixPath = tempDir.toString().replace("\\", "/"); |
| | | |
| | | String controllerDirectory = (String) invokePrivate(generator, "resolveControllerDirectory"); |
| | | invokePrivate( |
| | | generator, |
| | | "writeTemplate", |
| | | new Class<?>[]{String.class, String.class, String.class}, |
| | | new Object[]{"Controller", controllerDirectory, "ControllerCaseController.java"} |
| | | ); |
| | | |
| | | Path controllerFile = tempDir.resolve("src/main/java/com/vincent/rsf/server/system/controller/ControllerCaseController.java"); |
| | | String controllerContent = Files.readString(controllerFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(controllerContent.contains("public class ControllerCaseController extends BaseController"), "控制器类名应和实体保持一致"), |
| | | () -> assertTrue(controllerContent.contains("private ControllerCaseService controllerCaseService;"), "控制器应注入 service"), |
| | | () -> assertTrue(controllerContent.contains("private ListExportService listExportService;"), "控制器应注入导出服务"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/page\")"), "分页接口路径应保持 page"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/save\")"), "新增接口路径应保持 save"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/update\")"), "更新接口路径应保持 update"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/remove/{ids}\")"), "删除接口路径应保持 remove"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/query\")"), "查询接口路径应保持 query"), |
| | | () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/export\")"), "导出接口路径应保持 export"), |
| | | () -> assertTrue(controllerContent.contains("listExportService.export(map, exportMap -> buildParam(exportMap, BaseParam.class), controllerCaseExportHandler, response);"), "导出接口应继续走 ListExportService"), |
| | | () -> assertTrue(controllerContent.contains("controllerCase.setCreateBy(getLoginUserId());"), "新增初始化应补 createBy"), |
| | | () -> assertTrue(controllerContent.contains("controllerCase.setCreateTime(new Date());"), "新增初始化应补 createTime"), |
| | | () -> assertTrue(controllerContent.contains("controllerCase.setUpdateBy(getLoginUserId());"), "新增和更新初始化都应补 updateBy"), |
| | | () -> assertTrue(controllerContent.contains("controllerCase.setUpdateTime(new Date());"), "新增和更新初始化都应补 updateTime"), |
| | | () -> assertTrue(controllerContent.contains("row.put(\"statusText\", record.getStatus$());"), "导出行应输出 statusText"), |
| | | () -> assertTrue(controllerContent.contains("row.put(\"ownerIdText\", record.getOwnerId$());"), "导出行应输出外键文本字段"), |
| | | () -> assertTrue(controllerContent.contains("wrapper.like(ControllerCase::getName, condition);"), "query 接口应按主字段模糊查询"), |
| | | () -> assertTrue(controllerContent.contains("new KeyValVo(item.getId(), item.getName())"), "query 接口应返回主键和主字段") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void buildFrontendArtifactsKeepsApiRequestContractAligned() throws Exception { |
| | | RsfDesignGenerator generator = createGenerator( |
| | | "sys_api_case", |
| | | "接口测试表", |
| | | "apiCase", |
| | | "ApiCase", |
| | | "system/api-case", |
| | | "system/api-case", |
| | | buildColumns() |
| | | ); |
| | | |
| | | invokePrivate(generator, "buildFrontendArtifacts"); |
| | | |
| | | Path apiFile = tempDir.resolve("src/api/system/api-case.js"); |
| | | String apiContent = Files.readString(apiFile, StandardCharsets.UTF_8); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(apiContent.contains("function normalizeIds(ids) {"), "api 文件应包含 ids 归一化工具"), |
| | | () -> assertTrue(apiContent.contains("export function fetchApiCasePage(params = {}) {"), "应生成 page 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: '/apiCase/page'"), "page 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchApiCaseList(params = {}) {"), "应生成 list 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: '/apiCase/list'"), "list 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchGetApiCaseDetail(id) {"), "应生成 detail 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: `/apiCase/${id}`"), "detail 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchGetApiCaseMany(ids) {"), "应生成 many 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: `/apiCase/many/${normalizeIds(ids)}`"), "many 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchSaveApiCase(params = {}) {"), "应生成 save 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: '/apiCase/save'"), "save 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchUpdateApiCase(params = {}) {"), "应生成 update 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: '/apiCase/update'"), "update 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchDeleteApiCase(ids) {"), "应生成 delete 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: `/apiCase/remove/${normalizeIds(ids)}`"), "delete 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("export function fetchApiCaseQuery(condition = '') {"), "应生成 query 请求函数"), |
| | | () -> assertTrue(apiContent.contains("url: '/apiCase/query'"), "query 请求路径应正确"), |
| | | () -> assertTrue(apiContent.contains("condition: normalizeText(condition)"), "query 请求应对 condition 做 trim"), |
| | | () -> assertTrue(apiContent.contains("export async function fetchExportApiCaseReport(payload = {}, options = {}) {"), "应生成 export 请求函数"), |
| | | () -> assertTrue(apiContent.contains("fetch(`${import.meta.env.VITE_API_URL}/apiCase/export`"), "export 应直接请求完整导出地址"), |
| | | () -> assertTrue(apiContent.contains("method: 'POST'"), "export 请求应使用 POST"), |
| | | () -> assertTrue(apiContent.contains("body: JSON.stringify(payload)"), "export 请求应发送 JSON body") |
| | | ); |
| | | } |
| | | |
| | | @Test |
| | | void writeBackendArtifactsKeepEntityServiceMapperXmlAndSqlAligned() throws Exception { |
| | | ReactGenerator generator = createReactGenerator( |
| | | "sys_backend_case", |
| | | "后端测试表", |
| | | "backendCase", |
| | | "BackendCase", |
| | | buildBackendCaseColumns() |
| | | ); |
| | | |
| | | writeReactTemplate(generator, "Entity", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/entity").toString() + "/", "BackendCase.java"); |
| | | writeReactTemplate(generator, "Service", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service").toString() + "/", "BackendCaseService.java"); |
| | | writeReactTemplate(generator, "ServiceImpl", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/impl").toString() + "/", "BackendCaseServiceImpl.java"); |
| | | writeReactTemplate(generator, "Mapper", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/mapper").toString() + "/", "BackendCaseMapper.java"); |
| | | writeReactTemplate(generator, "Xml", tempDir.resolve("src/main/resources/mapper/system").toString() + "/", "BackendCaseMapper.xml"); |
| | | writeReactTemplate(generator, "Sql", tempDir.resolve("src/main/java").toString() + "/", "backendCase.sql"); |
| | | |
| | | Charset systemCharset = Charset.defaultCharset(); |
| | | String entityContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/entity/BackendCase.java"), systemCharset); |
| | | String serviceContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/BackendCaseService.java"), systemCharset); |
| | | String serviceImplContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/impl/BackendCaseServiceImpl.java"), systemCharset); |
| | | String mapperContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/mapper/BackendCaseMapper.java"), systemCharset); |
| | | String xmlContent = Files.readString(tempDir.resolve("src/main/resources/mapper/system/BackendCaseMapper.xml"), systemCharset); |
| | | String sqlContent = Files.readString(tempDir.resolve("src/main/java/backendCase.sql"), systemCharset); |
| | | |
| | | assertAll( |
| | | () -> assertTrue(entityContent.contains("@TableName(\"sys_backend_case\")"), "实体应绑定真实表名"), |
| | | () -> assertTrue(entityContent.contains("private Long id;"), "实体应生成主键字段"), |
| | | () -> assertTrue(entityContent.contains("private String name;"), "实体应生成普通文本字段"), |
| | | () -> assertTrue(entityContent.contains("private Long ownerId;"), "实体应生成外键字段"), |
| | | () -> assertTrue(entityContent.contains("private Integer status;"), "实体应生成状态字段"), |
| | | () -> assertTrue(entityContent.contains("@TableLogic"), "实体应为 deleted 字段添加逻辑删除注解"), |
| | | () -> assertTrue(entityContent.contains("@DateTimeFormat(pattern=\"yyyy-MM-dd HH:mm:ss\")"), "实体应为日期字段添加时间格式注解"), |
| | | () -> assertTrue(entityContent.contains("public Boolean getStatusBool(){"), "实体应保留 statusBool 便捷方法"), |
| | | () -> assertTrue(serviceContent.contains("public interface BackendCaseService extends IService<BackendCase>"), "Service 接口应继承 IService"), |
| | | () -> assertTrue(serviceImplContent.contains("public class BackendCaseServiceImpl extends ServiceImpl<BackendCaseMapper, BackendCase> implements BackendCaseService"), "ServiceImpl 应绑定 mapper 和 entity"), |
| | | () -> assertTrue(serviceImplContent.contains("@Service(\"backendCaseService\")"), "ServiceImpl bean 名应使用小驼峰"), |
| | | () -> assertTrue(mapperContent.contains("public interface BackendCaseMapper extends BaseMapper<BackendCase>"), "Mapper 应继承 BaseMapper"), |
| | | () -> assertTrue(xmlContent.contains("<mapper namespace=\"com.vincent.rsf.server.system.mapper.BackendCaseMapper\">"), "Xml namespace 应指向正确 mapper"), |
| | | () -> assertTrue(sqlContent.contains("insert into `sys_menu`"), "Sql 模板应生成菜单 SQL"), |
| | | () -> assertTrue(sqlContent.contains("'system:backendCase:list'"), "Sql 模板应生成 list 权限"), |
| | | () -> assertTrue(sqlContent.contains("'system:backendCase:save'"), "Sql 模板应生成 save 权限"), |
| | | () -> assertTrue(sqlContent.contains("'system:backendCase:update'"), "Sql 模板应生成 update 权限"), |
| | | () -> assertTrue(sqlContent.contains("'system:backendCase:remove'"), "Sql 模板应生成 remove 权限"), |
| | | () -> assertTrue(sqlContent.contains("backendCase: 'BackendCase'"), "Sql 模板应生成 locale 菜单名"), |
| | | () -> assertTrue(sqlContent.contains("import backendCase from './backendCase';"), "Sql 模板应生成资源导入片段") |
| | | ); |
| | | } |
| | | |
| | | private List<Column> buildColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); |
| | | Column level = new Column(null, "level", "Integer", "等级{1:高,0:低}", false, false, false, 11, false, SqlOsType.MYSQL); |
| | | Column roleId = new Column(null, "role_id", "Long", "角色", false, false, false, 20, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column createTime = new Column(null, "create_time", "Date", "添加时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | Column updateTime = new Column(null, "update_time", "Date", "修改时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); |
| | | |
| | | roleId.setForeignKeyMajor("Name"); |
| | | |
| | | return List.of(id, name, level, roleId, status, deleted, createTime, updateTime, memo); |
| | | } |
| | | |
| | | private List<Column> buildBooleanColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); |
| | | Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); |
| | | |
| | | return List.of(id, name, enabled, status, deleted, memo); |
| | | } |
| | | |
| | | private List<Column> buildDefaultCaseColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); |
| | | Column level = new Column(null, "level", "Integer", "等级", false, false, false, 11, false, SqlOsType.MYSQL); |
| | | Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); |
| | | |
| | | return List.of(id, name, level, enabled, status, deleted, memo); |
| | | } |
| | | |
| | | private List<Column> buildSearchCaseColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column keyword = new Column(null, "keyword", "String", "关键字字段", false, false, false, 255, false, SqlOsType.MYSQL); |
| | | Column level = new Column(null, "level", "Integer", "等级", false, false, false, 11, false, SqlOsType.MYSQL); |
| | | Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, false, 1, false, SqlOsType.MYSQL); |
| | | Column createTime = new Column(null, "create_time", "Date", "添加时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | |
| | | return List.of(id, keyword, level, enabled, status, createTime, deleted); |
| | | } |
| | | |
| | | private List<Column> buildControllerCaseColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); |
| | | Column ownerId = new Column(null, "owner_id", "Long", "负责人", false, false, false, 20, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column createBy = new Column(null, "create_by", "Long", "创建人", false, false, false, 20, false, SqlOsType.MYSQL); |
| | | Column createTime = new Column(null, "create_time", "Date", "创建时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | Column updateBy = new Column(null, "update_by", "Long", "更新人", false, false, false, 20, false, SqlOsType.MYSQL); |
| | | Column updateTime = new Column(null, "update_time", "Date", "更新时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | |
| | | ownerId.setForeignKeyMajor("Name"); |
| | | |
| | | return List.of(id, name, ownerId, status, createBy, createTime, updateBy, updateTime); |
| | | } |
| | | |
| | | private List<Column> buildBackendCaseColumns() { |
| | | Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); |
| | | Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); |
| | | Column ownerId = new Column(null, "owner_id", "Long", "负责人", false, false, false, 20, false, SqlOsType.MYSQL); |
| | | Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); |
| | | Column createTime = new Column(null, "create_time", "Date", "创建时间", false, false, false, null, false, SqlOsType.MYSQL); |
| | | ownerId.setForeignKey("User"); |
| | | ownerId.setForeignKeyMajor("Name"); |
| | | return List.of(id, name, ownerId, status, deleted, createTime); |
| | | } |
| | | |
| | | private RsfDesignGenerator createGenerator( |
| | | String table, |
| | | String tableDesc, |
| | | String simpleEntityName, |
| | | String fullEntityName, |
| | | String frontendViewPath, |
| | | String frontendApiModule, |
| | | List<Column> columns |
| | | ) throws Exception { |
| | | RsfDesignGenerator generator = new RsfDesignGenerator(); |
| | | generator.table = table; |
| | | generator.tableDesc = tableDesc; |
| | | generator.packagePath = "com.vincent.rsf.server.system"; |
| | | generator.frontendPrefixPath = tempDir.toString().replace("\\", "/"); |
| | | generator.frontendViewPath = frontendViewPath; |
| | | generator.frontendApiModule = frontendApiModule; |
| | | generator.sqlOsType = SqlOsType.MYSQL; |
| | | |
| | | setField(generator, "columns", columns); |
| | | setField(generator, "fullEntityName", fullEntityName); |
| | | setField(generator, "simpleEntityName", simpleEntityName); |
| | | setField(generator, "kebabEntityName", toKebab(simpleEntityName)); |
| | | setField(generator, "constantPrefix", simpleEntityName.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toUpperCase()); |
| | | setField(generator, "primaryKeyColumn", "id"); |
| | | setField(generator, "majorColumn", "name"); |
| | | setField(generator, "itemName", "system"); |
| | | setField(generator, "normalizedFrontendViewPath", frontendViewPath); |
| | | setField(generator, "normalizedFrontendApiModule", frontendApiModule); |
| | | return generator; |
| | | } |
| | | |
| | | private String toKebab(String value) { |
| | | return value.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(); |
| | | } |
| | | |
| | | private ReactGenerator createReactGenerator( |
| | | String table, |
| | | String tableDesc, |
| | | String simpleEntityName, |
| | | String fullEntityName, |
| | | List<Column> columns |
| | | ) throws Exception { |
| | | ReactGenerator generator = new ReactGenerator(); |
| | | generator.table = table; |
| | | generator.tableDesc = tableDesc; |
| | | generator.packagePath = "com.vincent.rsf.server.system"; |
| | | generator.backendPrefixPath = tempDir.toString().replace("\\", "/") + "/"; |
| | | generator.frontendPrefixPath = tempDir.toString().replace("\\", "/"); |
| | | |
| | | setField(generator, "columns", columns); |
| | | setField(generator, "fullEntityName", fullEntityName); |
| | | setField(generator, "simpleEntityName", simpleEntityName); |
| | | setField(generator, "itemName", "system"); |
| | | setField(generator, "systemPackagePath", "com.vincent.rsf.server.system"); |
| | | setField(generator, "systemPackage", "com.vincent.rsf.server.system"); |
| | | |
| | | setField(generator, "entityContent", invokePrivate(generator, "createEntityMsg")); |
| | | setField(generator, "primaryKeyColumn", invokePrivate(generator, "createPrimaryMsg")); |
| | | setField(generator, "majorColumn", invokePrivate(generator, "createMajorMsg")); |
| | | setField(generator, "reactLocaleContent", invokePrivate(generator, "createReactLocaleContent")); |
| | | return generator; |
| | | } |
| | | |
| | | private void writeReactTemplate(ReactGenerator generator, String templateName, String directory, String fileName) throws Exception { |
| | | String content = (String) invokePrivate(generator, "readFile", new Class<?>[]{String.class}, new Object[]{templateName}); |
| | | invokePrivate( |
| | | generator, |
| | | "writeFile", |
| | | new Class<?>[]{String.class, String.class, String.class, String.class}, |
| | | new Object[]{content, directory.replace("\\", "/"), fileName, templateName} |
| | | ); |
| | | } |
| | | |
| | | private void setField(Object target, String name, Object value) throws Exception { |
| | | Field field = target.getClass().getDeclaredField(name); |
| | | field.setAccessible(true); |
| | | field.set(target, value); |
| | | } |
| | | |
| | | private Object invokePrivate(Object target, String name) throws Exception { |
| | | Method method = target.getClass().getDeclaredMethod(name); |
| | | method.setAccessible(true); |
| | | return method.invoke(target); |
| | | } |
| | | |
| | | private Object invokePrivate(Object target, String name, Class<?>[] parameterTypes, Object[] args) throws Exception { |
| | | Method method = target.getClass().getDeclaredMethod(name, parameterTypes); |
| | | method.setAccessible(true); |
| | | return method.invoke(target, args); |
| | | } |
| | | } |