From 333a93571452073a9e628c6256044d345099aa50 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 02 四月 2026 08:19:55 +0800
Subject: [PATCH] #
---
rsf-design/src/components/core/layouts/art-page-content/index.vue | 24
rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js | 2
rsf-design/src/views/basic-info/warehouse-areas/index.vue | 81 +
rsf-design/src/views/manager/task/modules/task-expand-panel.vue | 143 +++
rsf-design/src/api/task.js | 16
rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js | 5
rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue | 175 +++
rsf-design/src/views/orders/transfer/index.vue | 12
rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java | 38
rsf-design/src/api/bas-station-area.js | 11
rsf-design/src/views/orders/transfer/transferPage.helpers.js | 1
rsf-design/src/views/orders/purchase/purchaseTable.columns.js | 10
rsf-design/src/main.js | 15
rsf-design/src/views/orders/out-stock/index.vue | 118 ++
rsf-design/src/views/orders/delivery/index.vue | 14
rsf-design/src/views/orders/out-stock/outStockPage.helpers.js | 5
rsf-design/src/views/orders/preparation/preparationPage.helpers.js | 1
rsf-design/src/views/orders/delivery/deliveryPage.helpers.js | 5
rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue | 46
rsf-design/src/views/orders/preparation/index.vue | 12
rsf-design/src/views/orders/preparation-item/index.vue | 74 +
rsf-design/src/views/system/menu/menuTable.columns.js | 20
rsf-design/src/api/out-stock-item.js | 7
rsf-design/src/views/manager/task/index.vue | 156 ++
rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js | 2
rsf-design/src/views/manager/task/modules/task-detail-drawer.vue | 71 +
rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js | 1
rsf-design/src/views/orders/purchase-item/index.vue | 63 +
rsf-design/src/views/orders/out-stock-item/index.vue | 70 +
rsf-design/src/views/orders/transfer-item/index.vue | 68 +
rsf-design/src/views/manager/task/taskPage.helpers.js | 50
rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue | 88 +
rsf-design/src/views/basic-info/bas-station-area/index.vue | 139 ++
rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue | 404 +++++++
rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue | 82
rsf-design/src/views/orders/delivery-item/index.vue | 67 +
rsf-design/src/views/orders/asn-order-item/index.vue | 80 +
rsf-server/src/test/java/com/vincent/rsf/server/common/RsfDesignGeneratorRegressionTest.java | 503 ++++++++++
rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js | 29
rsf-design/src/views/manager/task/taskTable.columns.js | 11
rsf-design/src/views/orders/purchase/index.vue | 15
rsf-design/src/views/orders/asn-order/index.vue | 61
42 files changed, 2,508 insertions(+), 287 deletions(-)
diff --git a/rsf-design/src/api/bas-station-area.js b/rsf-design/src/api/bas-station-area.js
index 7919a1a..aaa0acb 100644
--- a/rsf-design/src/api/bas-station-area.js
+++ b/rsf-design/src/api/bas-station-area.js
@@ -209,6 +209,17 @@
})
}
+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',
diff --git a/rsf-design/src/api/out-stock-item.js b/rsf-design/src/api/out-stock-item.js
index 591471d..b04e653 100644
--- a/rsf-design/src/api/out-stock-item.js
+++ b/rsf-design/src/api/out-stock-item.js
@@ -48,6 +48,13 @@
}
}
+ if (params.orderId !== '' && params.orderId !== undefined && params.orderId !== null) {
+ const numericOrderId = Number(params.orderId)
+ if (!Number.isNaN(numericOrderId)) {
+ result.orderId = numericOrderId
+ }
+ }
+
return result
}
diff --git a/rsf-design/src/api/task.js b/rsf-design/src/api/task.js
index 2157d26..a1f7a74 100644
--- a/rsf-design/src/api/task.js
+++ b/rsf-design/src/api/task.js
@@ -93,3 +93,19 @@
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'
+ }
+ })
+}
diff --git a/rsf-design/src/components/core/layouts/art-page-content/index.vue b/rsf-design/src/components/core/layouts/art-page-content/index.vue
index b98d865..5c1c4c3 100644
--- a/rsf-design/src/components/core/layouts/art-page-content/index.vue
+++ b/rsf-design/src/components/core/layouts/art-page-content/index.vue
@@ -22,27 +22,21 @@
</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>
- <KeepAlive :max="10" :exclude="keepAliveExclude">
- <component
- class="art-page-view"
- :is="Component"
- :key="route.path"
- v-if="route.meta.keepAlive"
- />
- </KeepAlive>
+ <div v-if="route.meta.keepAlive" class="art-page-view" :style="contentStyle">
+ <KeepAlive :max="10" :exclude="keepAliveExclude">
+ <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>
diff --git a/rsf-design/src/main.js b/rsf-design/src/main.js
index 8f704c3..2876cc7 100644
--- a/rsf-design/src/main.js
+++ b/rsf-design/src/main.js
@@ -13,21 +13,6 @@
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)
diff --git a/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
index 658dddb..c6c0c9b 100644
--- a/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
+++ b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
@@ -107,6 +107,8 @@
export function createBasStationAreaSearchState() {
return {
condition: '',
+ timeStart: '',
+ timeEnd: '',
stationAreaName: '',
stationAreaId: '',
type: '',
@@ -260,6 +262,8 @@
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:
@@ -472,3 +476,28 @@
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
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/bas-station-area/index.vue b/rsf-design/src/views/basic-info/bas-station-area/index.vue
index 34c283e..9ec13a5 100644
--- a/rsf-design/src/views/basic-info/bas-station-area/index.vue
+++ b/rsf-design/src/views/basic-info/bas-station-area/index.vue
@@ -24,6 +24,21 @@
>
鎵归噺鍒犻櫎
</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>
@@ -62,9 +77,12 @@
<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'
@@ -72,8 +90,10 @@
import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
import {
fetchBasStationAreaDetail,
+ fetchBasStationAreaMany,
fetchBasStationAreaPage,
fetchDeleteBasStationArea,
+ fetchExportBasStationAreaReport,
fetchSaveBasStationArea,
fetchUpdateBasStationArea
} from '@/api/bas-station-area'
@@ -81,8 +101,12 @@
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,
@@ -101,6 +125,7 @@
defineOptions({ name: 'BasStationArea' })
const { hasAuth } = useAuth()
+ const userStore = useUserStore()
const searchForm = ref(createBasStationAreaSearchState())
const detailDrawerVisible = ref(false)
@@ -162,6 +187,8 @@
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(() => [
{
@@ -171,6 +198,28 @@
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: '璇烽�夋嫨缁撴潫鏃堕棿'
}
},
{
@@ -248,12 +297,39 @@
}
},
{
+ 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: '璇疯緭鍏CS鏁版嵁'
+ }
+ },
+ {
+ label: '瀹瑰櫒绫诲瀷',
+ key: 'containerType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鍣ㄧ被鍨�'
}
},
{
@@ -272,6 +348,15 @@
props: {
clearable: true,
placeholder: '璇疯緭鍏ユ潯鐮�'
+ }
+ },
+ {
+ label: '绔欑偣鍒悕',
+ key: 'stationAlias',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰埆鍚�'
}
},
{
@@ -454,6 +539,60 @@
})
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()
diff --git a/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue
index c0f68ba..a4f8fbe 100644
--- a/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue
+++ b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue
@@ -2,60 +2,203 @@
<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>
- <div v-else class="space-y-4">
- <ElCard shadow="never" class="art-table-card">
+ <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 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>
+ <ElTag :type="detail.isCurrentType || 'info'" effect="light">
+ {{ detail.isCurrentText || '--' }}
+ </ElTag>
+ </ElSpace>
+ </div>
+ </ElCard>
+
+ <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">
- <div>
- <h3 class="m-0 text-base font-semibold">妯℃澘娴佺▼蹇収</h3>
- <p class="m-0 text-sm text-[var(--art-text-secondary)]">
- 杩欓噷灞曠ず鐨勬槸鍚庣妯℃澘瀛楁缁勫悎鍑虹殑鐪熷疄娴佺▼淇℃伅锛屼笉鍋氶澶栧亣鏁版嵁鎺ㄦ紨銆�
- </p>
- </div>
- <ElTag :type="detail.statusType || 'info'" effect="light">
- {{ detail.statusText || '--' }}
- </ElTag>
+ <span class="font-medium">妯℃澘鑺傜偣</span>
+ <span class="text-xs text-[var(--art-text-secondary)]">
+ {{ nodeLoading ? '鍔犺浇涓�' : `绗� ${nodePagination.current} 椤� / 鍏� ${nodePagination.total} 鏉 }}
+ </span>
</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 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>
- </div>
+ </template>
</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>
+ <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>
- </ElScrollbar>
+ </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 },
@@ -70,9 +213,194 @@
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>
diff --git a/rsf-design/src/views/basic-info/warehouse-areas/index.vue b/rsf-design/src/views/basic-info/warehouse-areas/index.vue
index 6f6b31c..e9ad55b 100644
--- a/rsf-design/src/views/basic-info/warehouse-areas/index.vue
+++ b/rsf-design/src/views/basic-info/warehouse-areas/index.vue
@@ -22,6 +22,21 @@
>
鎵归噺鍒犻櫎
</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>
@@ -59,10 +74,13 @@
<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'
@@ -70,7 +88,9 @@
fetchCompanysList,
fetchDeleteWarehouseAreas,
fetchWarehouseAreasDetail,
+ fetchWarehouseAreasMany,
fetchWarehouseAreasPage,
+ fetchExportWarehouseAreasReport,
fetchSaveWarehouseAreas,
fetchUpdateWarehouseAreas,
fetchWarehouseList
@@ -81,10 +101,13 @@
import {
buildWarehouseAreasDialogModel,
buildWarehouseAreasPageQueryParams,
+ buildWarehouseAreasPrintRows,
buildWarehouseAreasSavePayload,
buildWarehouseAreasSearchParams,
createWarehouseAreasSearchState,
getWarehouseAreasPaginationKey,
+ WAREHOUSE_AREAS_REPORT_STYLE,
+ WAREHOUSE_AREAS_REPORT_TITLE,
getWarehouseAreasStatusOptions,
normalizeWarehouseAreasDetailRecord,
normalizeWarehouseAreasListRow
@@ -93,6 +116,7 @@
defineOptions({ name: 'WarehouseAreas' })
const { hasAuth } = useAuth()
+ const userStore = useUserStore()
const searchForm = ref(createWarehouseAreasSearchState())
const detailDrawerVisible = ref(false)
@@ -164,6 +188,9 @@
}
}
])
+
+ const reportTitle = WAREHOUSE_AREAS_REPORT_TITLE
+ const reportQueryParams = computed(() => buildWarehouseAreasSearchParams(searchForm.value))
async function openDetail(row) {
detailDrawerVisible.value = true
@@ -249,6 +276,60 @@
})
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()
diff --git a/rsf-design/src/views/manager/task/index.vue b/rsf-design/src/views/manager/task/index.vue
index 138697d..08fc6cf 100644
--- a/rsf-design/src/views/manager/task/index.vue
+++ b/rsf-design/src/views/manager/task/index.vue
@@ -9,7 +9,30 @@
/>
<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"
@@ -21,6 +44,11 @@
/>
</ElCard>
+ <TaskFlowStepDialog
+ v-model:visible="flowStepDialogVisible"
+ :task-row="activeTaskRow"
+ />
+
<TaskDetailDrawer
v-model:visible="detailDrawerVisible"
:loading="detailLoading"
@@ -31,13 +59,14 @@
@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 {
@@ -45,12 +74,15 @@
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,
@@ -70,6 +102,10 @@
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,
@@ -192,15 +228,29 @@
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
}
@@ -237,7 +287,17 @@
}
}
- 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)
@@ -273,6 +333,33 @@
}
}
+ 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
@@ -280,31 +367,29 @@
detailLoading.value = true
try {
- const [detailResponse, taskItemResponse] = await Promise.all([
- guardRequestWithMessage(fetchTaskDetail(activeTaskRow.value.id), {}, {
- timeoutMessage: '浠诲姟璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ const taskItemResponse = await guardRequestWithMessage(
+ fetchTaskItemPage({
+ taskId: activeTaskRow.value.id,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
}),
- guardRequestWithMessage(
- fetchTaskItemPage({
- taskId: activeTaskRow.value.id,
- current: detailPagination.current,
- pageSize: detailPagination.size
- }),
- {
- records: [],
- total: 0,
- current: detailPagination.current,
- size: detailPagination.size
- },
- { timeoutMessage: '浠诲姟鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
- )
- ])
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ { 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
}
@@ -348,5 +433,26 @@
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>
diff --git a/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue b/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
index 7a65cb6..53cee56 100644
--- a/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
+++ b/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
@@ -6,27 +6,58 @@
@update:model-value="handleVisibleChange"
>
<div class="flex h-full flex-col gap-4">
- <ElDescriptions :column="4" border>
- <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskCode || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="浠诲姟鐘舵��">{{ detail.taskStatusLabel || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="浠诲姟绫诲瀷">{{ detail.taskTypeLabel || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="璁惧绫诲瀷">{{ detail.warehTypeLabel || '--' }}</ElDescriptionsItem>
- <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>
+ <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>
+ </ElDescriptions>
+ </ElCard>
+ </div>
<div class="flex items-center justify-between">
- <div class="text-sm text-[var(--art-gray-600)]">浠诲姟鏄庣粏</div>
- <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ <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
@@ -51,7 +82,7 @@
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)
diff --git a/rsf-design/src/views/manager/task/modules/task-expand-panel.vue b/rsf-design/src/views/manager/task/modules/task-expand-panel.vue
new file mode 100644
index 0000000..9028100
--- /dev/null
+++ b/rsf-design/src/views/manager/task/modules/task-expand-panel.vue
@@ -0,0 +1,143 @@
+<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>
diff --git a/rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue b/rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue
new file mode 100644
index 0000000..98b15bf
--- /dev/null
+++ b/rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue
@@ -0,0 +1,175 @@
+<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>
diff --git a/rsf-design/src/views/manager/task/taskPage.helpers.js b/rsf-design/src/views/manager/task/taskPage.helpers.js
index 0fa5bda..bf7eb7c 100644
--- a/rsf-design/src/views/manager/task/taskPage.helpers.js
+++ b/rsf-design/src/views/manager/task/taskPage.helpers.js
@@ -55,7 +55,8 @@
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
}
}
@@ -84,12 +85,25 @@
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
? [
@@ -121,19 +135,27 @@
}
]
: []),
- {
- key: 'top',
- label: '浠诲姟缃《',
- icon: 'ri:pushpin-line',
- auth: 'update'
- },
- {
- key: 'remove',
- label: '鍙栨秷浠诲姟',
- icon: 'ri:close-circle-line',
- color: '#f56c6c',
- auth: 'delete'
- }
+ ...(canTopTask(row)
+ ? [
+ {
+ key: 'top',
+ label: '浠诲姟缃《',
+ icon: 'ri:pushpin-line',
+ auth: 'update'
+ }
+ ]
+ : []),
+ ...(row.canCancel
+ ? [
+ {
+ key: 'remove',
+ label: '鍙栨秷浠诲姟',
+ icon: 'ri:close-circle-line',
+ color: '#f56c6c',
+ auth: 'delete'
+ }
+ ]
+ : [])
]
}
diff --git a/rsf-design/src/views/manager/task/taskTable.columns.js b/rsf-design/src/views/manager/task/taskTable.columns.js
index 6482c27..d924763 100644
--- a/rsf-design/src/views/manager/task/taskTable.columns.js
+++ b/rsf-design/src/views/manager/task/taskTable.columns.js
@@ -2,8 +2,17 @@
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: '浠诲姟鍙�',
diff --git a/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js b/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js
index d5c43d2..dbf84a9 100644
--- a/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js
+++ b/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js
@@ -78,6 +78,7 @@
export function createAsnOrderItemSearchState() {
return {
condition: '',
+ orderId: '',
orderCode: '',
poCode: '',
platWorkCode: '',
@@ -109,6 +110,7 @@
;[
'condition',
+ 'orderId',
'orderCode',
'poCode',
'platWorkCode',
diff --git a/rsf-design/src/views/orders/asn-order-item/index.vue b/rsf-design/src/views/orders/asn-order-item/index.vue
index f1f4cce..a10b8b2 100644
--- a/rsf-design/src/views/orders/asn-order-item/index.vue
+++ b/rsf-design/src/views/orders/asn-order-item/index.vue
@@ -1,5 +1,15 @@
<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>鍏ュ簱閫氱煡鍗旾D锛歿{ activeSourceSummary.orderId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -49,8 +59,9 @@
</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'
@@ -81,6 +92,8 @@
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([])
@@ -88,6 +101,15 @@
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),
@@ -274,6 +296,32 @@
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)
@@ -383,4 +431,32 @@
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>
diff --git a/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js b/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
index 0edd1f6..6bf4f06 100644
--- a/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
+++ b/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
@@ -245,6 +245,11 @@
icon: 'ri:eye-line'
},
{
+ key: 'items',
+ label: '鏀惰揣鏄庣粏',
+ icon: 'ri:list-check-3'
+ },
+ {
key: 'print',
label: '鎵撳嵃',
icon: 'ri:printer-line'
diff --git a/rsf-design/src/views/orders/asn-order/index.vue b/rsf-design/src/views/orders/asn-order/index.vue
index 1ef8bd6..c771230 100644
--- a/rsf-design/src/views/orders/asn-order/index.vue
+++ b/rsf-design/src/views/orders/asn-order/index.vue
@@ -60,6 +60,7 @@
<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'
@@ -72,7 +73,6 @@
fetchAsnOrderPage,
fetchCompleteAsnOrder,
fetchExportAsnOrderReport,
- fetchGetAsnOrderDetail,
fetchGetAsnOrderMany
} from '@/api/asn-order'
import AsnOrderDetailDrawer from './modules/asn-order-detail-drawer.vue'
@@ -98,6 +98,7 @@
defineOptions({ name: 'AsnOrder' })
const userStore = useUserStore()
+ const router = useRouter()
const reportTitle = ASN_ORDER_REPORT_TITLE
const searchForm = ref(createAsnOrderSearchState())
const selectedRows = ref([])
@@ -106,6 +107,7 @@
const detailData = ref({})
const detailTableData = ref([])
const activeOrderId = ref(null)
+ const activeOrderRow = ref(null)
const poDialogVisible = ref(false)
const detailPagination = reactive({
@@ -184,6 +186,8 @@
async function openDetail(row) {
activeOrderId.value = row.id
+ activeOrderRow.value = row
+ detailData.value = normalizeAsnOrderRow(row)
detailPagination.current = 1
detailDrawerVisible.value = true
await loadDetailResources()
@@ -202,6 +206,16 @@
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
}
@@ -267,35 +281,26 @@
detailLoading.value = true
try {
- const [detailResponse, itemResponse] = await Promise.all([
- guardRequestWithMessage(
- fetchGetAsnOrderDetail(activeOrderId.value),
- {},
- {
- timeoutMessage: '鍏ュ簱閫氱煡鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
- }
- ),
- guardRequestWithMessage(
- fetchAsnOrderItemPage(
- buildAsnOrderDetailQueryParams({
- orderId: activeOrderId.value,
- current: detailPagination.current,
- pageSize: detailPagination.size
- })
- ),
- {
- records: [],
- total: 0,
+ const itemResponse = await guardRequestWithMessage(
+ fetchAsnOrderItemPage(
+ buildAsnOrderDetailQueryParams({
+ orderId: activeOrderId.value,
current: detailPagination.current,
- size: detailPagination.size
- },
- {
- timeoutMessage: '鍏ュ簱閫氱煡鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
- }
- )
- ])
+ pageSize: detailPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ {
+ timeoutMessage: '鍏ュ簱閫氱煡鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
- detailData.value = normalizeAsnOrderRow(detailResponse)
+ detailData.value = normalizeAsnOrderRow(activeOrderRow.value || {})
detailTableData.value = Array.isArray(itemResponse?.records)
? itemResponse.records.map((item) => normalizeAsnOrderItemRow(item))
: []
diff --git a/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue b/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
index f9c0ebe..a5534ca 100644
--- a/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
+++ b/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
@@ -2,54 +2,48 @@
<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>
- <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.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="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
- </ElDescriptions>
+ <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.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="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
- <div class="flex items-center justify-between">
- <div class="text-sm text-[var(--art-gray-600)]"
- >鏄庣粏娓呭崟锛堢墿鏂欑紪鐮�/鐗╂枡鍚嶇О/渚涘簲鍟嗘壒娆★級</div
- >
- <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ <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">鍏� {{ data.length }} 鏉�</ElTag>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
</div>
-
- <ArtTable
- :loading="loading"
- :data="data"
- :columns="columns"
- :pagination="pagination"
- @pagination:size-change="$emit('size-change', $event)"
- @pagination:current-change="$emit('current-change', $event)"
- />
- </div>
+ </ElScrollbar>
</ElDrawer>
</template>
diff --git a/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js b/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
index c38a44f..afe7c69 100644
--- a/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
+++ b/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
@@ -37,6 +37,7 @@
export function createDeliveryItemSearchState() {
return {
condition: '',
+ deliveryId: '',
deliveryCode: '',
platItemId: '',
matnrCode: '',
diff --git a/rsf-design/src/views/orders/delivery-item/index.vue b/rsf-design/src/views/orders/delivery-item/index.vue
index eea4e50..fdd6cc6 100644
--- a/rsf-design/src/views/orders/delivery-item/index.vue
+++ b/rsf-design/src/views/orders/delivery-item/index.vue
@@ -1,5 +1,15 @@
<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鍗旾D锛歿{ activeSourceSummary.deliveryId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -30,8 +40,9 @@
</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'
@@ -45,10 +56,21 @@
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(() => [
{
@@ -175,4 +197,45 @@
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>
diff --git a/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js b/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
index c770e3c..e993d8e 100644
--- a/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
+++ b/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
@@ -248,6 +248,11 @@
key: 'view',
label: '鏌ョ湅璇︽儏',
icon: 'ri:eye-line'
+ },
+ {
+ key: 'items',
+ label: '鏄庣粏',
+ icon: 'ri:list-check-3'
}
]
diff --git a/rsf-design/src/views/orders/delivery/index.vue b/rsf-design/src/views/orders/delivery/index.vue
index 851a0d5..f3ec265 100644
--- a/rsf-design/src/views/orders/delivery/index.vue
+++ b/rsf-design/src/views/orders/delivery/index.vue
@@ -55,6 +55,7 @@
<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'
@@ -87,6 +88,7 @@
defineOptions({ name: 'Delivery' })
const userStore = useUserStore()
+ const router = useRouter()
const reportTitle = 'DO鍗曟姤琛�'
const searchForm = ref(createDeliverySearchState())
const selectedRows = ref([])
@@ -267,6 +269,18 @@
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)
}
diff --git a/rsf-design/src/views/orders/out-stock-item/index.vue b/rsf-design/src/views/orders/out-stock-item/index.vue
index 58ecafa..03c29d0 100644
--- a/rsf-design/src/views/orders/out-stock-item/index.vue
+++ b/rsf-design/src/views/orders/out-stock-item/index.vue
@@ -1,5 +1,15 @@
<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>鍑哄簱鍗旾D锛歿{ activeSourceSummary.orderId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -30,8 +40,9 @@
</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'
@@ -45,10 +56,24 @@
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(() => [
{
@@ -214,4 +239,45 @@
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>
diff --git a/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js b/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js
index 27cffa4..7f92e10 100644
--- a/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js
+++ b/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js
@@ -43,6 +43,7 @@
export function createOutStockItemSearchState() {
return {
condition: '',
+ orderId: '',
orderCode: '',
poCode: '',
platItemId: '',
@@ -75,6 +76,7 @@
].forEach((key) => pushText(result, key, params[key]))
pushNumber(result, 'status', params.status)
+ pushNumber(result, 'orderId', params.orderId)
return result
}
diff --git a/rsf-design/src/views/orders/out-stock/index.vue b/rsf-design/src/views/orders/out-stock/index.vue
index 5f73851..ab3fd34 100644
--- a/rsf-design/src/views/orders/out-stock/index.vue
+++ b/rsf-design/src/views/orders/out-stock/index.vue
@@ -39,19 +39,32 @@
/>
</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,
@@ -73,17 +86,30 @@
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(() => [
@@ -138,22 +164,69 @@
{ 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) => {
- detailDrawerVisible.value = false
- detailData.value = {}
- ElMessage.error(error?.message || '鑾峰彇鍑哄簱鍗曡鎯呭け璐�')
- })
- .finally(() => {
- detailLoading.value = false
- })
+ detailItemPagination.current = 1
+ detailData.value = {}
+ detailItemRows.value = []
+ try {
+ await loadDetailResources()
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailItemRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇鍑哄簱鍗曡鎯呭け璐�')
+ }
+ }
+
+ function handleDetailSizeChange(size) {
+ detailItemPagination.size = size
+ detailItemPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailItemPagination.current = current
+ loadDetailResources()
}
async function handleComplete(row) {
@@ -167,7 +240,7 @@
ElMessage.success('瀹屾垚鎴愬姛')
await refreshData()
if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
- openDetail(row)
+ await loadDetailResources()
}
} catch (error) {
if (error === 'cancel' || error?.message === 'cancel') return
@@ -186,7 +259,7 @@
ElMessage.success('鍙栨秷鎴愬姛')
await refreshData()
if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
- openDetail(row)
+ await loadDetailResources()
}
} catch (error) {
if (error === 'cancel' || error?.message === 'cancel') return
@@ -216,6 +289,15 @@
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
diff --git a/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue b/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue
index feebb70..98a20e1 100644
--- a/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue
+++ b/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue
@@ -2,14 +2,14 @@
<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>
@@ -33,12 +33,31 @@
<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>
@@ -47,6 +66,7 @@
<script setup>
import { computed } from 'vue'
+ import ArtTable from '@/components/core/tables/art-table/index.vue'
defineOptions({ name: 'OutStockDetailDrawer' })
@@ -59,13 +79,29 @@
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,
diff --git a/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
index d31e6ed..19012d1 100644
--- a/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
+++ b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
@@ -157,6 +157,11 @@
icon: 'ri:eye-line'
},
{
+ key: 'items',
+ label: '鏄庣粏',
+ icon: 'ri:list-check-3'
+ },
+ {
key: 'print',
label: '鎵撳嵃',
icon: 'ri:printer-line'
diff --git a/rsf-design/src/views/orders/preparation-item/index.vue b/rsf-design/src/views/orders/preparation-item/index.vue
index 07a12ef..60cb842 100644
--- a/rsf-design/src/views/orders/preparation-item/index.vue
+++ b/rsf-design/src/views/orders/preparation-item/index.vue
@@ -1,5 +1,15 @@
<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>澶囨枡鍗旾D锛歿{ activeSourceSummary.orderId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -49,9 +59,9 @@
</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'
@@ -82,6 +92,7 @@
defineOptions({ name: 'PreparationItem' })
const route = useRoute()
+ const router = useRouter()
const userStore = useUserStore()
const initialOrderId = route.query.orderId || route.query.id
const searchForm = ref(
@@ -96,6 +107,18 @@
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(() => [
{
@@ -247,6 +270,45 @@
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
@@ -297,4 +359,10 @@
previewMeta.value?.reportStyle?.orientation || PREPARATION_ITEM_REPORT_STYLE.orientation
})
)
+
+ onMounted(() => {
+ applyRouteSearch()
+ replaceSearchParams(buildPreparationItemPageQueryParams(searchForm.value))
+ getData()
+ })
</script>
diff --git a/rsf-design/src/views/orders/preparation/index.vue b/rsf-design/src/views/orders/preparation/index.vue
index d918f5b..1e162a0 100644
--- a/rsf-design/src/views/orders/preparation/index.vue
+++ b/rsf-design/src/views/orders/preparation/index.vue
@@ -58,6 +58,7 @@
<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'
@@ -93,6 +94,7 @@
defineOptions({ name: 'Preparation' })
const userStore = useUserStore()
+ const router = useRouter()
const reportTitle = PREPARATION_REPORT_TITLE
const searchForm = ref(createPreparationSearchState())
const selectedRows = ref([])
@@ -230,6 +232,16 @@
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: '纭畾',
diff --git a/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue b/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue
index 8e2c335..e5c7cf2 100644
--- a/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue
+++ b/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue
@@ -2,48 +2,60 @@
<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>
- <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>
- </ElDescriptionsItem>
- <ElDescriptionsItem label="閲婃斁鐘舵��">
- <ElTag :type="detail.rleStatusTagType || 'info'">{{ detail.rleStatusText || '--' }}</ElTag>
- </ElDescriptionsItem>
- <ElDescriptionsItem label="鐗╂祦鍗曞彿">{{ detail.logisNo || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="涓氬姟鏃堕棿">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="閿�鍞粍缁�">{{ detail.saleOrgName || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="閿�鍞憳">{{ detail.saleUserName || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="瀹㈡埛缂栫爜">{{ detail.customerId || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="瀹㈡埛鍚嶇О">{{ detail.customerName || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="浠撳簱缁勭粐">{{ detail.stockOrgName || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="搴斿嚭鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
- </ElDescriptions>
+ <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'" effect="light">
+ {{ detail.exceStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="閲婃斁鐘舵��">
+ <ElTag :type="detail.rleStatusTagType || 'info'" effect="light">
+ {{ detail.rleStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂祦鍗曞彿">{{ detail.logisNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟鏃堕棿">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞粍缁�">{{ detail.saleOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞憳">{{ detail.saleUserName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛缂栫爜">{{ detail.customerId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛鍚嶇О">{{ detail.customerName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱缁勭粐">{{ detail.stockOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴斿嚭鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
- <div class="flex items-center justify-between">
- <div class="text-sm text-[var(--art-gray-600)]">鏄庣粏娓呭崟锛堢墿鏂欑紪鐮�/鐗╂枡鍚嶇О/渚涘簲鍟嗘壒娆★級</div>
- <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ <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">鍏� {{ data.length }} 鏉�</ElTag>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
</div>
-
- <ArtTable
- :loading="loading"
- :data="data"
- :columns="columns"
- :pagination="pagination"
- @pagination:size-change="$emit('size-change', $event)"
- @pagination:current-change="$emit('current-change', $event)"
- />
- </div>
+ </ElScrollbar>
</ElDrawer>
</template>
diff --git a/rsf-design/src/views/orders/preparation/preparationPage.helpers.js b/rsf-design/src/views/orders/preparation/preparationPage.helpers.js
index 5a42752..6278cca 100644
--- a/rsf-design/src/views/orders/preparation/preparationPage.helpers.js
+++ b/rsf-design/src/views/orders/preparation/preparationPage.helpers.js
@@ -186,6 +186,7 @@
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',
diff --git a/rsf-design/src/views/orders/purchase-item/index.vue b/rsf-design/src/views/orders/purchase-item/index.vue
index 8ded304..f120beb 100644
--- a/rsf-design/src/views/orders/purchase-item/index.vue
+++ b/rsf-design/src/views/orders/purchase-item/index.vue
@@ -1,5 +1,15 @@
<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鍗旾D锛歿{ activeSourceSummary.purchaseId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -48,8 +58,9 @@
</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'
@@ -79,12 +90,23 @@
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(() => [
{
@@ -306,6 +328,29 @@
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 {
@@ -364,7 +409,21 @@
})
)
+ 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>
diff --git a/rsf-design/src/views/orders/purchase/index.vue b/rsf-design/src/views/orders/purchase/index.vue
index 35b855d..c3c6463 100644
--- a/rsf-design/src/views/orders/purchase/index.vue
+++ b/rsf-design/src/views/orders/purchase/index.vue
@@ -74,6 +74,7 @@
<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'
@@ -119,6 +120,7 @@
const { hasAuth } = useAuth()
const userStore = useUserStore()
+ const router = useRouter()
const reportTitle = PURCHASE_REPORT_TITLE
const searchForm = ref(createPurchaseSearchState())
@@ -276,6 +278,18 @@
}
}
+ function openPurchaseItems(row) {
+ if (!row?.id) {
+ return
+ }
+ router.push({
+ path: '/orders/purchase-item',
+ query: {
+ purchaseId: String(row.id)
+ }
+ })
+ }
+
const {
columns,
columnChecks,
@@ -299,6 +313,7 @@
columnsFactory: () =>
createPurchaseTableColumns({
handleView: openDetail,
+ handleViewItems: openPurchaseItems,
handleEdit: hasAuth('update') ? openEditDialog : null,
handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
canEdit: hasAuth('update'),
diff --git a/rsf-design/src/views/orders/purchase/purchaseTable.columns.js b/rsf-design/src/views/orders/purchase/purchaseTable.columns.js
index a279aff..2d81a20 100644
--- a/rsf-design/src/views/orders/purchase/purchaseTable.columns.js
+++ b/rsf-design/src/views/orders/purchase/purchaseTable.columns.js
@@ -5,6 +5,7 @@
export function createPurchaseTableColumns({
handleView,
+ handleViewItems,
handleEdit,
handleDelete,
canEdit = true,
@@ -152,6 +153,14 @@
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' })
}
@@ -169,6 +178,7 @@
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)
}
diff --git a/rsf-design/src/views/orders/transfer-item/index.vue b/rsf-design/src/views/orders/transfer-item/index.vue
index 47bff6c..4981645 100644
--- a/rsf-design/src/views/orders/transfer-item/index.vue
+++ b/rsf-design/src/views/orders/transfer-item/index.vue
@@ -1,5 +1,15 @@
<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>璋冩嫧鍗旾D锛歿{ activeSourceSummary.transferId }}</span>
+ </div>
+ <ElButton link type="primary" @click="handleClearSourceFilter">鏌ョ湅鍏ㄩ儴</ElButton>
+ </div>
+ </ElCard>
+
<ArtSearchBar
v-model="searchForm"
:items="searchItems"
@@ -48,8 +58,9 @@
</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'
@@ -80,12 +91,23 @@
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(() => [
{
@@ -372,6 +394,29 @@
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
@@ -425,5 +470,22 @@
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>
diff --git a/rsf-design/src/views/orders/transfer/index.vue b/rsf-design/src/views/orders/transfer/index.vue
index f85b2d4..8a42133 100644
--- a/rsf-design/src/views/orders/transfer/index.vue
+++ b/rsf-design/src/views/orders/transfer/index.vue
@@ -77,6 +77,7 @@
<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'
@@ -129,6 +130,7 @@
const { hasAuth } = useAuth()
const userStore = useUserStore()
+ const router = useRouter()
const reportTitle = TRANSFER_REPORT_TITLE
const searchForm = ref(createTransferSearchState())
@@ -296,6 +298,16 @@
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)
diff --git a/rsf-design/src/views/orders/transfer/transferPage.helpers.js b/rsf-design/src/views/orders/transfer/transferPage.helpers.js
index 7e06d2c..0d11a6a 100644
--- a/rsf-design/src/views/orders/transfer/transferPage.helpers.js
+++ b/rsf-design/src/views/orders/transfer/transferPage.helpers.js
@@ -306,6 +306,7 @@
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) {
diff --git a/rsf-design/src/views/system/menu/menuTable.columns.js b/rsf-design/src/views/system/menu/menuTable.columns.js
index a27b767..e98239c 100644
--- a/rsf-design/src/views/system/menu/menuTable.columns.js
+++ b/rsf-design/src/views/system/menu/menuTable.columns.js
@@ -62,6 +62,16 @@
}
},
{
+ prop: 'component',
+ label: '缁勪欢鏍囪瘑',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => {
+ if (row.meta?.isAuthButton) return ''
+ return row.component || ''
+ }
+ },
+ {
prop: 'authority',
label: '鏉冮檺鏍囪瘑',
minWidth: 180,
@@ -77,6 +87,12 @@
prop: 'sort',
label: '鎺掑簭',
width: 90
+ },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 96,
+ align: 'center'
},
{
prop: 'status',
@@ -98,9 +114,9 @@
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, {
diff --git a/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
index c805452..3d428e3 100644
--- a/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
+++ b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
@@ -186,7 +186,7 @@
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() {
@@ -207,6 +207,14 @@
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 {
@@ -677,14 +685,6 @@
.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())
@@ -695,6 +695,14 @@
.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("', '")
@@ -703,7 +711,7 @@
.append(resolveTextColumnWidth(column))
.append("),\n");
}
- return trimTrailingLineBreak(sb);
+ return trimTrailingLineBreakKeepComma(sb);
}
private String buildExportRowContent() {
@@ -898,6 +906,16 @@
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();
}
diff --git a/rsf-server/src/test/java/com/vincent/rsf/server/common/RsfDesignGeneratorRegressionTest.java b/rsf-server/src/test/java/com/vincent/rsf/server/common/RsfDesignGeneratorRegressionTest.java
new file mode 100644
index 0000000..14cf07d
--- /dev/null
+++ b/rsf-server/src/test/java/com/vincent/rsf/server/common/RsfDesignGeneratorRegressionTest.java
@@ -0,0 +1,503 @@
+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 妯″潡璺緞搴旂敓鎴愬埌姝g‘鐩綍")
+ );
+ }
+
+ @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:姝e父,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:姝e父,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:姝e父,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:姝e父,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:姝e父,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:姝e父,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);
+ }
+}
--
Gitblit v1.9.1