Просмотр исходного кода

feat(s5): 标签查询只读列表 + 菜单重指

S5z 库存数据/标签查询:新增 MissedPrint 只读分页查询(list/status-options/location-options),LEFT JOIN GeneralizedCodeMaster 解码 BarcodeStatus;前端只读列表页(7查询项/25列/分页/状态·库位下拉);标签查询菜单 placeholder 重指真实页(UpdateScripts/1.0.216.sql + SysMenuSeedData 双落)。数据源 aidopdev.MissedPrint 只读,无 DDL、无写路径。

chore: bump version Web 2.4.211 / server 1.0.216
YY968XX 1 неделя назад
Родитель
Сommit
06675f581e

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "admin.net",
   "type": "module",
-  "version": "2.4.210",
+  "version": "2.4.211",
   "packageManager": "pnpm@10.32.1",
   "lastBuildTime": "2026.03.15",
   "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 91 - 0
Web/src/views/aidop/s5/api/labelQuery.ts

@@ -0,0 +1,91 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+// ── S5 标签查询(只读,数据源 aidopdev.MissedPrint)──
+export interface LabelQueryRow {
+	id?: number;
+	/** 条码 BarCode */
+	barCode?: string;
+	/** 物料编码 ItemNum */
+	itemNum?: string;
+	/** 描述 Descr */
+	descr?: string;
+	/** 规格型号 Product */
+	product?: string;
+	/** 数量 Qty */
+	qty?: number;
+	/** 状态码 Status */
+	status?: string;
+	/** 状态描述 StatusDesc(BarcodeStatus 解码) */
+	statusDesc?: string;
+	/** 批次号 LotSerial */
+	lotSerial?: string;
+	/** 库位 Location */
+	location?: string;
+	/** 货架 Shelf */
+	shelf?: string;
+	/** 单据号 OrdNbr */
+	ordNbr?: string;
+	/** 包装数量 PackingQty(默认隐藏列) */
+	packingQty?: number;
+	/** 生产日期 ProdDate */
+	prodDate?: string;
+	/** 工单编号 WorkOrd */
+	workOrd?: string;
+	/** 生产线 ProdLine */
+	prodLine?: string;
+	/** 标签格式 LabelFormat */
+	labelFormat?: string;
+	/** 位置 Position(默认隐藏列) */
+	position?: string;
+	/** 箱号 Carton */
+	carton?: string;
+	/** 班次 Period */
+	period?: number;
+	/** 供应商 Supply */
+	supply?: string;
+	/** 采购单号 PurOrd */
+	purOrd?: string;
+	/** 采购单项次 PurLine */
+	purLine?: number;
+	/** 送货单号 ShipperNbr */
+	shipperNbr?: string;
+	/** 送货单项次 ShipperLine */
+	shipperLine?: number;
+	/** 入库日期 InvDate */
+	invDate?: string;
+	/** 过期日期 ExpireDate */
+	expireDate?: string;
+	/** 备注 Remark */
+	remark?: string;
+}
+
+export interface LabelQueryStatusOption {
+	val?: string;
+	label?: string;
+}
+
+export interface LabelQueryLocationOption {
+	val?: string;
+}
+
+export function fetchLabelQueryList(params: any) {
+	return service
+		.get<Paged<LabelQueryRow>>('/api/S5LabelQuery/list', { params: withAidopTenantParams(params) })
+		.then((r) => r.data);
+}
+
+export function fetchLabelQueryStatusOptions() {
+	return service.get<LabelQueryStatusOption[]>('/api/S5LabelQuery/status-options').then((r) => r.data);
+}
+
+export function fetchLabelQueryLocationOptions() {
+	return service.get<LabelQueryLocationOption[]>('/api/S5LabelQuery/location-options').then((r) => r.data);
+}

+ 213 - 0
Web/src/views/aidop/s5/inventory/labelQueryList.vue

@@ -0,0 +1,213 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="只读列表">
+		<!--
+			S5 标签查询 · 只读页(S5z-LABEL-QUERY-1)。
+			数据源:aidopdev.MissedPrint(legacy 标签/条码记录表,只读);状态描述经 GeneralizedCodeMaster BarcodeStatus 解码。
+			只读:无新增/编辑/保存/库位转移/库存事务/状态流转;无 mock。
+		-->
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="物料编码">
+				<el-input v-model="query.itemNum" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="批次号">
+				<el-input v-model="query.lotSerial" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="库位">
+				<el-select v-model="query.location" clearable placeholder="请选择" style="width: 140px">
+					<el-option v-for="o in locationOptions" :key="o.val" :label="o.val" :value="o.val!" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="货架">
+				<el-input v-model="query.shelf" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="送货单号">
+				<el-input v-model="query.shipperNbr" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="状态">
+				<el-select v-model="query.status" clearable placeholder="请选择" style="width: 140px">
+					<el-option v-for="o in statusOptions" :key="o.val" :label="o.label" :value="o.val!" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="条码">
+				<el-input v-model="query.barCode" clearable style="width: 220px" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doSearch">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" row-key="id" v-loading="loading" border stripe @sort-change="onSortChange">
+			<el-table-column type="index" label="#" width="55" align="center" />
+			<el-table-column prop="barCode" label="条码" min-width="200" sortable="custom" show-overflow-tooltip resizable />
+			<el-table-column prop="itemNum" label="物料编码" min-width="130" sortable="custom" show-overflow-tooltip resizable />
+			<el-table-column prop="descr" label="描述" min-width="140" show-overflow-tooltip resizable />
+			<el-table-column prop="product" label="规格型号" min-width="140" show-overflow-tooltip resizable />
+			<el-table-column prop="qty" label="数量" width="90" align="right" sortable="custom" resizable />
+			<el-table-column prop="status" label="状态" width="80" sortable="custom" resizable />
+			<el-table-column prop="statusDesc" label="状态描述" width="110" show-overflow-tooltip resizable />
+			<el-table-column prop="lotSerial" label="批次号" min-width="140" sortable="custom" show-overflow-tooltip resizable />
+			<el-table-column prop="location" label="库位" width="90" sortable="custom" resizable />
+			<el-table-column prop="shelf" label="货架" width="120" show-overflow-tooltip resizable />
+			<el-table-column prop="ordNbr" label="单据号" min-width="140" show-overflow-tooltip resizable />
+			<el-table-column prop="prodDate" label="生产日期" width="120" sortable="custom" resizable>
+				<template #default="{ row }">{{ fmtDate(row.prodDate) }}</template>
+			</el-table-column>
+			<el-table-column prop="workOrd" label="工单编号" min-width="130" show-overflow-tooltip resizable />
+			<el-table-column prop="prodLine" label="生产线" width="100" resizable />
+			<el-table-column prop="labelFormat" label="标签格式" width="110" resizable />
+			<el-table-column prop="carton" label="箱号" width="100" resizable />
+			<el-table-column prop="period" label="班次" width="80" align="right" resizable />
+			<el-table-column prop="supply" label="供应商" min-width="120" show-overflow-tooltip resizable />
+			<el-table-column prop="purOrd" label="采购单号" min-width="130" show-overflow-tooltip resizable />
+			<el-table-column prop="purLine" label="采购单项次" width="110" align="right" resizable />
+			<el-table-column prop="shipperNbr" label="送货单号" min-width="130" show-overflow-tooltip resizable />
+			<el-table-column prop="shipperLine" label="送货单项次" width="110" align="right" resizable />
+			<el-table-column prop="invDate" label="入库日期" width="120" sortable="custom" resizable>
+				<template #default="{ row }">{{ fmtDate(row.invDate) }}</template>
+			</el-table-column>
+			<el-table-column prop="expireDate" label="过期日期" width="120" sortable="custom" resizable>
+				<template #default="{ row }">{{ fmtDate(row.expireDate) }}</template>
+			</el-table-column>
+			<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip resizable />
+			<template #empty>
+				<el-empty description="暂无数据" />
+			</template>
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[10, 20, 50]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS5InventoryLabelQuery">
+import { computed, onActivated, onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import {
+	fetchLabelQueryList,
+	fetchLabelQueryStatusOptions,
+	fetchLabelQueryLocationOptions,
+	type LabelQueryRow,
+	type LabelQueryStatusOption,
+	type LabelQueryLocationOption,
+} from '../api/labelQuery';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '标签查询');
+
+const query = reactive({
+	itemNum: '',
+	lotSerial: '',
+	location: '',
+	shelf: '',
+	shipperNbr: '',
+	status: '',
+	barCode: '',
+	page: 1,
+	pageSize: 10,
+	orderBy: '',
+	orderDir: '',
+});
+
+const loading = ref(false);
+const rows = ref<LabelQueryRow[]>([]);
+const total = ref(0);
+const statusOptions = ref<LabelQueryStatusOption[]>([]);
+const locationOptions = ref<LabelQueryLocationOption[]>([]);
+
+function fmtDate(v?: string | null) {
+	if (!v) return '';
+	return String(v).slice(0, 10);
+}
+
+function onSortChange({ prop, order }: { prop: string; order: string | null }) {
+	query.orderBy = prop || '';
+	query.orderDir = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : '';
+	loadList();
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchLabelQueryList({
+			itemNum: query.itemNum,
+			lotSerial: query.lotSerial,
+			location: query.location,
+			shelf: query.shelf,
+			shipperNbr: query.shipperNbr,
+			status: query.status,
+			barCode: query.barCode,
+			page: query.page,
+			pageSize: query.pageSize,
+			orderBy: query.orderBy,
+			orderDir: query.orderDir,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} catch (e: any) {
+		rows.value = [];
+		total.value = 0;
+		ElMessage.error(e?.message || '加载标签查询列表失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
+async function loadOptions() {
+	try {
+		const [st, loc] = await Promise.all([fetchLabelQueryStatusOptions(), fetchLabelQueryLocationOptions()]);
+		statusOptions.value = st || [];
+		locationOptions.value = loc || [];
+	} catch {
+		statusOptions.value = [];
+		locationOptions.value = [];
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+
+function resetQuery() {
+	query.itemNum = '';
+	query.lotSerial = '';
+	query.location = '';
+	query.shelf = '';
+	query.shipperNbr = '';
+	query.status = '';
+	query.barCode = '';
+	query.page = 1;
+	query.orderBy = '';
+	query.orderDir = '';
+	loadList();
+}
+
+onMounted(() => {
+	loadOptions();
+	loadList();
+});
+onActivated(() => loadList());
+</script>
+
+<style scoped lang="scss">
+.mb12 {
+	margin-bottom: 12px;
+}
+.pager {
+	margin-top: 12px;
+	display: flex;
+	justify-content: flex-end;
+}
+</style>

+ 6 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.215</AssemblyVersion>
-    <FileVersion>1.0.215</FileVersion>
-    <Version>1.0.215</Version>
+    <AssemblyVersion>1.0.216</AssemblyVersion>
+    <FileVersion>1.0.216</FileVersion>
+    <Version>1.0.216</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -205,6 +205,9 @@
     <None Update="UpdateScripts\1.0.215.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.216.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 15 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.216.sql

@@ -0,0 +1,15 @@
+-- ============================================================================
+-- S5z 标签查询(S5z-LABEL-QUERY-1)
+-- 作用:将「标签查询」菜单(Id=1329015030001,库存数据目录 1322000000017 下)
+--       的 Component 由 placeholder 重指到真实页面 /aidop/s5/inventory/labelQueryList。
+-- 数据源:aidopdev.MissedPrint(legacy 标签/条码记录表,只读);状态描述经
+--         GeneralizedCodeMaster(FldName='BarcodeStatus') 解码。本页只读,无 DDL、无业务表。
+-- 约束:仅 UPDATE 该单条 Component / Remark / UpdateTime;不改 Path/Name/Pid/Title/Type/OrderNo;
+--       幂等,可重复执行。
+-- ============================================================================
+
+UPDATE SysMenu
+SET Component  = '/aidop/s5/inventory/labelQueryList',
+    Remark     = 'S5 标签查询(只读列表,数据源 aidopdev.MissedPrint)',
+    UpdateTime = NOW()
+WHERE Id = 1329015030001;

+ 154 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/LabelQueryDto.cs

@@ -0,0 +1,154 @@
+namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
+
+/// <summary>
+/// S5 标签查询 列表查询入参(只读)。
+///
+/// 数据源:aidopdev.MissedPrint(legacy 标签/条码记录表,只读)。
+/// 状态描述经 GeneralizedCodeMaster(FldName='BarcodeStatus') 解码。
+/// MissedPrint 无 tenant_id 列,TenantId 仅为前端 withAidopTenantParams 注入的透传参数,本查询不据其过滤(对齐既有 MissedPrint 只读投影先例,按工厂域隔离为后续独立议题)。
+/// </summary>
+public class LabelQueryListInput
+{
+    /// <summary>页码(从 1 开始)</summary>
+    public int Page { get; set; } = 1;
+
+    /// <summary>每页条数</summary>
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>物料编码(ItemNum,模糊匹配)</summary>
+    public string? ItemNum { get; set; }
+
+    /// <summary>批次号(LotSerial,模糊匹配)</summary>
+    public string? LotSerial { get; set; }
+
+    /// <summary>库位(Location,精确匹配,下拉选择)</summary>
+    public string? Location { get; set; }
+
+    /// <summary>货架(Shelf,模糊匹配)</summary>
+    public string? Shelf { get; set; }
+
+    /// <summary>送货单号(ShipperNbr,模糊匹配)</summary>
+    public string? ShipperNbr { get; set; }
+
+    /// <summary>状态(Status,精确匹配,下拉选择)</summary>
+    public string? Status { get; set; }
+
+    /// <summary>条码(BarCode,模糊匹配)</summary>
+    public string? BarCode { get; set; }
+
+    /// <summary>排序字段(前端列 prop:barCode/itemNum/qty/status/lotSerial/location/prodDate/invDate/expireDate)</summary>
+    public string? OrderBy { get; set; }
+
+    /// <summary>排序方向(asc / desc)</summary>
+    public string? OrderDir { get; set; }
+
+    /// <summary>租户 ID(前端 withAidopTenantParams 注入;MissedPrint 无租户列,本查询忽略,不据此过滤)</summary>
+    public long? TenantId { get; set; }
+}
+
+/// <summary>
+/// S5 标签查询 列表行(只读)。字段对齐前端列 prop(camelCase),源列见注释。
+/// </summary>
+public class LabelQueryListRow
+{
+    /// <summary>主键 id(MissedPrint.RecID)</summary>
+    public long Id { get; set; }
+
+    /// <summary>条码(BarCode)</summary>
+    public string? BarCode { get; set; }
+
+    /// <summary>物料编码(ItemNum)</summary>
+    public string? ItemNum { get; set; }
+
+    /// <summary>描述(Descr)</summary>
+    public string? Descr { get; set; }
+
+    /// <summary>规格型号(Product)</summary>
+    public string? Product { get; set; }
+
+    /// <summary>数量(Qty)</summary>
+    public decimal? Qty { get; set; }
+
+    /// <summary>状态码(Status)</summary>
+    public string? Status { get; set; }
+
+    /// <summary>状态描述(GeneralizedCodeMaster BarcodeStatus 解码;无解码回落原码)</summary>
+    public string? StatusDesc { get; set; }
+
+    /// <summary>批次号(LotSerial)</summary>
+    public string? LotSerial { get; set; }
+
+    /// <summary>库位(Location)</summary>
+    public string? Location { get; set; }
+
+    /// <summary>货架(Shelf)</summary>
+    public string? Shelf { get; set; }
+
+    /// <summary>单据号(OrdNbr)</summary>
+    public string? OrdNbr { get; set; }
+
+    /// <summary>包装数量(PackingQty,默认隐藏列)</summary>
+    public decimal? PackingQty { get; set; }
+
+    /// <summary>生产日期(ProdDate)</summary>
+    public DateTime? ProdDate { get; set; }
+
+    /// <summary>工单编号(WorkOrd)</summary>
+    public string? WorkOrd { get; set; }
+
+    /// <summary>生产线(ProdLine)</summary>
+    public string? ProdLine { get; set; }
+
+    /// <summary>标签格式(LabelFormat)</summary>
+    public string? LabelFormat { get; set; }
+
+    /// <summary>位置(Position,默认隐藏列)</summary>
+    public string? Position { get; set; }
+
+    /// <summary>箱号(Carton)</summary>
+    public string? Carton { get; set; }
+
+    /// <summary>班次(Period)</summary>
+    public int? Period { get; set; }
+
+    /// <summary>供应商(Supply)</summary>
+    public string? Supply { get; set; }
+
+    /// <summary>采购单号(PurOrd)</summary>
+    public string? PurOrd { get; set; }
+
+    /// <summary>采购单项次(PurLine)</summary>
+    public int? PurLine { get; set; }
+
+    /// <summary>送货单号(ShipperNbr)</summary>
+    public string? ShipperNbr { get; set; }
+
+    /// <summary>送货单项次(ShipperLine)</summary>
+    public int? ShipperLine { get; set; }
+
+    /// <summary>入库日期(InvDate)</summary>
+    public DateTime? InvDate { get; set; }
+
+    /// <summary>过期日期(ExpireDate)</summary>
+    public DateTime? ExpireDate { get; set; }
+
+    /// <summary>备注(Remark)</summary>
+    public string? Remark { get; set; }
+}
+
+/// <summary>状态下拉选项(BarcodeStatus 码表:Val=状态码,Label=中文描述)。</summary>
+public class LabelQueryStatusOption
+{
+    /// <summary>状态码(GeneralizedCodeMaster.Val)</summary>
+    public string? Val { get; set; }
+
+    /// <summary>中文描述(GeneralizedCodeMaster.Comments)</summary>
+    public string? Label { get; set; }
+}
+
+/// <summary>库位下拉选项(MissedPrint.Location distinct)。</summary>
+public class LabelQueryLocationOption
+{
+    /// <summary>库位(MissedPrint.Location)</summary>
+    public string? Val { get; set; }
+}

+ 180 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/LabelQueryService.cs

@@ -0,0 +1,180 @@
+using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
+
+namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
+
+/// <summary>
+/// S5 标签查询 只读 list 服务。
+///
+/// 数据源:aidopdev.MissedPrint(legacy 标签/条码记录表)。状态描述经 GeneralizedCodeMaster(FldName='BarcodeStatus') 解码。
+/// 参考本仓既有同表只读投影先例:Controllers/S0/Manufacturing/AdoS0MfgProcessFlowCardsController(同解 BarcodeStatus)。
+///
+/// 本服务仅 SELECT:无新增/编辑/删除/库位转移/库存事务/状态流转/物料卡写入;扁平列表,无 detail。
+/// MissedPrint 无 tenant_id 列,不做租户/工厂域过滤(对齐既有 MissedPrint 只读投影先例;按工厂域隔离为后续独立议题)。
+/// </summary>
+[ApiDescriptionSettings(Order = 302, Description = "标签查询")]
+[Route("api/S5LabelQuery")]
+[AllowAnonymous]
+[NonUnify]
+public class LabelQueryService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    public LabelQueryService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>
+    /// 标签查询列表(只读分页查询)。
+    /// </summary>
+    [DisplayName("标签查询列表")]
+    [HttpGet("list")]
+    public async Task<object> GetList([FromQuery] LabelQueryListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var where = new List<string> { "1=1" };
+        var pars = new List<SugarParameter>();
+
+        if (!string.IsNullOrWhiteSpace(input.ItemNum))
+        {
+            where.Add("m.ItemNum LIKE @ItemNum");
+            pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.LotSerial))
+        {
+            where.Add("m.LotSerial LIKE @LotSerial");
+            pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Location))
+        {
+            where.Add("m.Location = @Location");
+            pars.Add(new SugarParameter("@Location", input.Location.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Shelf))
+        {
+            where.Add("m.Shelf LIKE @Shelf");
+            pars.Add(new SugarParameter("@Shelf", $"%{input.Shelf.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ShipperNbr))
+        {
+            where.Add("m.ShipperNbr LIKE @ShipperNbr");
+            pars.Add(new SugarParameter("@ShipperNbr", $"%{input.ShipperNbr.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Status))
+        {
+            where.Add("m.Status = @Status");
+            pars.Add(new SugarParameter("@Status", input.Status.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.BarCode))
+        {
+            where.Add("m.BarCode LIKE @BarCode");
+            pars.Add(new SugarParameter("@BarCode", $"%{input.BarCode.Trim()}%"));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+
+        var total = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", pars);
+
+        var list = await _db.Ado.SqlQueryAsync<LabelQueryListRow>(
+            $"""
+            SELECT
+                m.RecID       AS Id,
+                m.BarCode     AS BarCode,
+                m.ItemNum     AS ItemNum,
+                m.Descr       AS Descr,
+                m.Product     AS Product,
+                m.Qty         AS Qty,
+                m.Status      AS Status,
+                COALESCE(g.Comments, m.Status) AS StatusDesc,
+                m.LotSerial   AS LotSerial,
+                m.Location    AS Location,
+                m.Shelf       AS Shelf,
+                m.OrdNbr      AS OrdNbr,
+                m.PackingQty  AS PackingQty,
+                m.ProdDate    AS ProdDate,
+                m.WorkOrd     AS WorkOrd,
+                m.ProdLine    AS ProdLine,
+                m.LabelFormat AS LabelFormat,
+                m.Position    AS Position,
+                m.Carton      AS Carton,
+                m.Period      AS Period,
+                m.Supply      AS Supply,
+                m.PurOrd      AS PurOrd,
+                m.PurLine     AS PurLine,
+                m.ShipperNbr  AS ShipperNbr,
+                m.ShipperLine AS ShipperLine,
+                m.InvDate     AS InvDate,
+                m.ExpireDate  AS ExpireDate,
+                m.Remark      AS Remark
+            FROM MissedPrint m
+            LEFT JOIN GeneralizedCodeMaster g ON g.FldName = 'BarcodeStatus' AND g.Val = m.Status
+            WHERE {whereSql}
+            ORDER BY {BuildOrderBy(input.OrderBy, input.OrderDir)}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    /// <summary>
+    /// 状态下拉选项(BarcodeStatus 码表,Val + 中文描述,只读)。
+    /// </summary>
+    [DisplayName("标签查询状态选项")]
+    [HttpGet("status-options")]
+    public async Task<object> GetStatusOptions()
+    {
+        var list = await _db.Ado.SqlQueryAsync<LabelQueryStatusOption>(
+            """
+            SELECT Val AS Val, Comments AS Label
+            FROM GeneralizedCodeMaster
+            WHERE FldName = 'BarcodeStatus' AND Comments IS NOT NULL AND Comments <> ''
+            GROUP BY Val, Comments
+            ORDER BY Val
+            """);
+        return list;
+    }
+
+    /// <summary>
+    /// 库位下拉选项(MissedPrint.Location distinct,只读)。
+    /// </summary>
+    [DisplayName("标签查询库位选项")]
+    [HttpGet("location-options")]
+    public async Task<object> GetLocationOptions()
+    {
+        var list = await _db.Ado.SqlQueryAsync<LabelQueryLocationOption>(
+            """
+            SELECT DISTINCT Location AS Val
+            FROM MissedPrint
+            WHERE Location IS NOT NULL AND Location <> ''
+            ORDER BY Location
+            """);
+        return list;
+    }
+
+    /// <summary>
+    /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。
+    /// </summary>
+    private static string BuildOrderBy(string? orderBy, string? orderDir)
+    {
+        var column = orderBy switch
+        {
+            "barCode" => "m.BarCode",
+            "itemNum" => "m.ItemNum",
+            "qty" => "m.Qty",
+            "status" => "m.Status",
+            "lotSerial" => "m.LotSerial",
+            "location" => "m.Location",
+            "prodDate" => "m.ProdDate",
+            "invDate" => "m.InvDate",
+            "expireDate" => "m.ExpireDate",
+            _ => "m.CreateTime",
+        };
+        var direction = string.Equals(orderDir, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return $"{column} {direction}, m.RecID DESC";
+    }
+}

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -549,7 +549,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         // 库存数据 (1322000000017) 下 6 个三级
         const long inventoryDirId = 1322000000017L;
         const long inventoryBase = 1329015030000L;
-        yield return new SysMenu { Id = inventoryBase + 1, Pid = inventoryDirId, Title = "标签查询", Path = "/aidop/s5/inventory/label-query", Name = "aidopS5InventoryLabelQuery", Component = placeholderComponent, Icon = "ele-Search", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 10, Remark = "S5 标签查询(真实页面待交付)" };
+        yield return new SysMenu { Id = inventoryBase + 1, Pid = inventoryDirId, Title = "标签查询", Path = "/aidop/s5/inventory/label-query", Name = "aidopS5InventoryLabelQuery", Component = "/aidop/s5/inventory/labelQueryList", Icon = "ele-Search", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 10, Remark = "S5 标签查询(只读列表,数据源 aidopdev.MissedPrint)" };
         yield return new SysMenu { Id = inventoryBase + 2, Pid = inventoryDirId, Title = "暂收在检列表", Path = "/aidop/s5/inventory/pending-inspection", Name = "aidopS5InventoryPendingInspection", Component = placeholderComponent, Icon = "ele-Document", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 20, Remark = "S5 暂收在检列表(真实页面待交付)" };
         yield return new SysMenu { Id = inventoryBase + 3, Pid = inventoryDirId, Title = "库存查询", Path = "/aidop/s5/inventory/stock-query", Name = "aidopS5InventoryStockQuery", Component = placeholderComponent, Icon = "ele-Search", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 30, Remark = "S5 库存查询(真实页面待交付)" };
         yield return new SysMenu { Id = inventoryBase + 4, Pid = inventoryDirId, Title = "进出存查询", Path = "/aidop/s5/inventory/inout-query", Name = "aidopS5InventoryInoutQuery", Component = placeholderComponent, Icon = "ele-Search", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 40, Remark = "S5 进出存查询(真实页面待交付)" };