Browse Source

采购做完,保存一下

张洋 1 day ago
parent
commit
a9f5e0f922

BIN
bin/Win32/Debug/zhipuzi_pos_windows/DuiLib_d.dll


+ 323 - 11
bin/Win32/Debug/zhipuzi_pos_windows/skin/caigou_page.xml

@@ -1,15 +1,327 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Window>
+	<!-- 采购页面:无一级 tab;周期行(区间 + 时段分段,右端)+ 概览带 4 格 + 二级 ovseg(采购记录 / 采购统计)
+	     + 筛选行(状态下拉 / 搜索 分类 查询 重置)+ 记录卡 / 统计卡 + 新建采购单独立整页。
+	     全部圆角走 8x 超采样 PNG 九宫格,不使用 borderround(仅 3px 色条小竖条用 borderround="3,3")。
+	     注意:main.xml 的全局 Default 给所有 Button 套 btn_green_* 三态图,非绿按钮须显式写 normalimage / hotimage / pushedimage 覆盖。 -->
+	<Default name="Combo" value="itemhottextcolor=&quot;#FF60B177&quot; itemhotbkcolor=&quot;#FFF2F7F4&quot; itemselectedtextcolor=&quot;#FF60B177&quot; itemselectedbkcolor=&quot;#FFE8F5EE&quot;" />
 	<CaigouPage>
-		<HorizontalLayout>
-			<Control width="1" bkcolor="#FFD1D1D1"></Control>
-			
-			<VerticalLayout name="shangpin_food_layout">
-				<Control></Control>
-				<Label text="采购功能暂未上线,敬请期待" align="center" font="20" padding="0,0,0,0"></Label>
-				<Control></Control>
-			</VerticalLayout>
-				
-		</HorizontalLayout>
+		<VerticalLayout name="cg_page" bkcolor="#FFF5F7FA" childpadding="0">
+
+			<!-- 整页滚动:覆盖含周期行在内的一整页;右 inset 归 0,右侧 28px 留白由每个直接子项自己 padding="0,0,28,0" 承担;
+			     fixedscrollbar="true" 让滚动条显隐不改变内容区宽度(与库存页 kc_scroll 同构)。
+			     视图容器高度由 C++ SyncTableHeight 写死到视图容器为止。 -->
+			<VerticalLayout name="cg_scroll" vscrollbar="true" fixedscrollbar="true" bkcolor="#FFF5F7FA" inset="28,18,0,24" childpadding="8">
+
+				<!-- 周期行:概览带上方独立一行,左端留空、右端周期区间 + 时段分段(照入库页 tab 行右端位置语义;采购页无 tab 故左端空) -->
+				<HorizontalLayout name="cg_perrow" height="36" padding="0,0,28,0" childvalign="vcenter">
+					<Control />
+					<Label name="cg_per_range" text="2026-10-01 ~ 2026-10-31" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" textpadding="0,0,14,0" />
+					<HorizontalLayout name="cg_tabseg" width="206" maxheight="36" bkimage="file='kc_ovseg_track.png' corner='8,8,8,8'" inset="3,3,3,3" childpadding="2" childvalign="vcenter">
+						<Button name="cg_per_0" text="本月" font="12" textcolor="#FF2E9B5C" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="56" maxheight="30" align="center" normalimage="file='kc_ovseg_on_n.png' corner='6,6,6,6'" hotimage="file='kc_ovseg_on_h.png' corner='6,6,6,6'" pushedimage="file='kc_ovseg_on_p.png' corner='6,6,6,6'" />
+						<Button name="cg_per_1" text="上月" font="12" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="56" maxheight="30" align="center" normalimage="" hotimage="file='kc_ovseg_off_h.png' corner='6,6,6,6'" pushedimage="file='kc_ovseg_off_h.png' corner='6,6,6,6'" />
+						<Button name="cg_per_2" text="近三个月" font="12" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="84" maxheight="30" align="center" normalimage="" hotimage="file='kc_ovseg_off_h.png' corner='6,6,6,6'" pushedimage="file='kc_ovseg_off_h.png' corner='6,6,6,6'" />
+					</HorizontalLayout>
+				</HorizontalLayout>
+
+				<!-- 概览带:单容器 bb_card10.png + 表头 + 发丝线 + 4 格(采购单数 / 采购金额 / 待审核 / 已拒绝) -->
+				<VerticalLayout name="cg_ov_band" height="131" padding="0,0,28,0" bkimage="file='bb_card10.png' corner='10,10,10,10'" childpadding="0">
+					<HorizontalLayout height="46" padding="18,0,18,0" childpadding="10" childvalign="vcenter">
+						<Control width="3" maxheight="14" bkcolor="#FF3CB371" borderround="3,3" />
+						<Label text="数据概览" font="114" textcolor="#FF1F2329" maxheight="20" autocalcwidth="true" />
+						<Label text="自然月" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" />
+						<Control />
+					</HorizontalLayout>
+					<Control height="1" bkcolor="#FFEEF0F3" />
+					<HorizontalLayout height="84" childpadding="0">
+						<VerticalLayout padding="18,16,18,16" childpadding="0">
+							<HorizontalLayout height="20" childvalign="vcenter" childpadding="8">
+								<Control width="3" maxheight="13" bkcolor="#FFC9CDD2" borderround="3,3" />
+								<Label text="采购单数" font="13" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+							</HorizontalLayout>
+							<Control height="4" />
+							<Label name="cg_ov_total" showhtml="true" font="126" textcolor="#FF1F2329" height="28" valign="center" text="--{x 4}{c #8A9099}{f 13}单{/f}{/c}" />
+						</VerticalLayout>
+						<Control width="1" bkcolor="#FFEEF0F3" />
+						<VerticalLayout padding="18,16,18,16" childpadding="0">
+							<HorizontalLayout height="20" childvalign="vcenter" childpadding="8">
+								<Control width="3" maxheight="13" bkcolor="#FFC9CDD2" borderround="3,3" />
+								<Label text="采购金额" font="13" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+							</HorizontalLayout>
+							<Control height="4" />
+							<Label name="cg_ov_amount" showhtml="true" font="126" textcolor="#FF1F2329" height="28" valign="center" text="--{x 4}{c #8A9099}{f 13}元{/f}{/c}" />
+						</VerticalLayout>
+						<Control width="1" bkcolor="#FFEEF0F3" />
+						<VerticalLayout padding="18,16,18,16" childpadding="0">
+							<HorizontalLayout height="20" childvalign="vcenter" childpadding="8">
+								<Control width="3" maxheight="13" bkcolor="#FFE08A17" borderround="3,3" />
+								<Label text="待审核" font="13" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+							</HorizontalLayout>
+							<Control height="4" />
+							<Label name="cg_ov_audit" showhtml="true" font="126" textcolor="#FF1F2329" height="28" valign="center" text="--{x 4}{c #8A9099}{f 13}单{/f}{/c}" />
+						</VerticalLayout>
+						<Control width="1" bkcolor="#FFEEF0F3" />
+						<VerticalLayout padding="18,16,18,16" childpadding="0">
+							<HorizontalLayout height="20" childvalign="vcenter" childpadding="8">
+								<Control width="3" maxheight="13" bkcolor="#FFE14B4A" borderround="3,3" />
+								<Label text="已拒绝" font="13" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+							</HorizontalLayout>
+							<Control height="4" />
+							<Label name="cg_ov_reject" showhtml="true" font="126" textcolor="#FF1F2329" height="28" valign="center" text="--{x 4}{c #8A9099}{f 13}单{/f}{/c}" />
+						</VerticalLayout>
+					</HorizontalLayout>
+				</VerticalLayout>
+
+				<!-- 二级分段:采购记录 / 采购统计(照 kc_rk_seg_*:kc_segcard 白底胶囊条 + kc_seggrn_* 绿胶囊选中 / 灰字未选) -->
+				<HorizontalLayout name="cg_ovseg" height="42" padding="0,0,28,0" childvalign="vcenter">
+					<HorizontalLayout width="182" maxheight="42" bkimage="file='kc_segcard.png' corner='21,21,21,21'" inset="5,5,5,5" childpadding="4" childvalign="vcenter">
+						<Button name="cg_seg_rec" text="采购记录" font="113" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="84" maxheight="32" align="center" normalimage="file='kc_seggrn_n.png' corner='16,16,16,16'" hotimage="file='kc_seggrn_h.png' corner='16,16,16,16'" pushedimage="file='kc_seggrn_p.png' corner='16,16,16,16'" />
+						<Button name="cg_seg_stat" text="采购统计" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="84" maxheight="32" align="center" normalimage="" hotimage="file='kc_seggray_h.png' corner='16,16,16,16'" pushedimage="file='kc_seggray_p.png' corner='16,16,16,16'" />
+					</HorizontalLayout>
+					<Control />
+				</HorizontalLayout>
+
+				<!-- 筛选行:两态共用同一行。记录态 = 状态下拉 + 右侧「新建采购单」;统计态 = 搜索 + 分类 + 查询 + 重置 + 右侧「新建采购单」。不可见子项被横排跳过不占位。 -->
+				<HorizontalLayout name="cg_frow" height="40" padding="0,0,28,0" childpadding="12" childvalign="vcenter">
+					<HorizontalLayout name="cg_frow_rec" childpadding="12" childvalign="vcenter">
+						<Combo name="cg_rec_combo_status" width="140" height="40" maxheight="40" normalimage="file='gd_combo40_n.png' corner='8,8,22,8'" hotimage="file='gd_combo40_h.png' corner='8,8,22,8'" pushedimage="file='gd_combo40_p.png' corner='8,8,22,8'" textpadding="11,0,30,0" />
+						<Control />
+						<Button name="cg_rec_new_btn" text="新建采购单" font="14" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="128" maxheight="40" align="center" normalimage="file='btn_green_zhengchang.png' corner='8,8,8,8'" hotimage="file='btn_green_xuanfu.png' corner='8,8,8,8'" pushedimage="file='btn_green_dianji.png' corner='8,8,8,8'" />
+					</HorizontalLayout>
+					<HorizontalLayout name="cg_frow_stat" visible="false" childpadding="12" childvalign="vcenter">
+						<HorizontalLayout name="cg_stat_search_box" width="240" bkimage="file='kc_search_box.png' corner='8,8,8,8'" inset="12,0,12,0" childpadding="8" childvalign="vcenter">
+							<Control bkimage="dc_ico_search16.png" width="16" height="16" maxheight="16" />
+							<Edit name="cg_stat_search_edit" tooltip="搜索商品名称/拼音码" font="12" textcolor="#FF1F2329" maxheight="24" bkcolor="#00000000" nativebkcolor="#FFFFFFFF" />
+							<Label name="cg_stat_search_tishi" text="搜索商品名称/拼音码" width="180" height="20" font="12" float="true" pos="40,10,220,30" mouse="false" valign="center" textcolor="#FFA8ADB3" />
+						</HorizontalLayout>
+						<Combo name="cg_stat_combo_cat" width="150" height="40" maxheight="40" normalimage="file='gd_combo40_n.png' corner='8,8,22,8'" hotimage="file='gd_combo40_h.png' corner='8,8,22,8'" pushedimage="file='gd_combo40_p.png' corner='8,8,22,8'" textpadding="11,0,30,0" />
+						<Button name="cg_stat_query_btn" text="查询" font="13" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="72" maxheight="40" align="center" normalimage="file='btn_green_zhengchang.png' corner='8,8,8,8'" hotimage="file='btn_green_xuanfu.png' corner='8,8,8,8'" pushedimage="file='btn_green_dianji.png' corner='8,8,8,8'" />
+						<Button name="cg_stat_reset_btn" text="重置" font="13" textcolor="#FF5F6368" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" width="72" maxheight="40" align="center" normalimage="file='kc_btn40_gho_n.png' corner='8,8,8,8'" hotimage="file='kc_btn40_gho_h.png' corner='8,8,8,8'" pushedimage="file='kc_btn40_gho_p.png' corner='8,8,8,8'" />
+						<Control />
+						<Button name="cg_stat_new_btn" text="新建采购单" font="14" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="128" maxheight="40" align="center" normalimage="file='btn_green_zhengchang.png' corner='8,8,8,8'" hotimage="file='btn_green_xuanfu.png' corner='8,8,8,8'" pushedimage="file='btn_green_dianji.png' corner='8,8,8,8'" />
+					</HorizontalLayout>
+				</HorizontalLayout>
+
+				<!-- 采购记录卡:表头行 + 表格卡(kc_tblcard.png 烤进 1px 外框 + 43px 表头灰带 + 表头下 1px 分隔线)+ 页脚 -->
+				<VerticalLayout name="cg_rec_body" padding="0,0,28,0" bkimage="file='bb_card10.png' corner='10,10,10,10'" inset="22,18,22,18" childpadding="0">
+					<HorizontalLayout height="22" childvalign="vcenter">
+						<Control width="3" maxheight="15" bkcolor="#FF2E9B5C" borderround="3,3" />
+						<Control width="10" />
+						<Label text="采购记录" font="116" textcolor="#FF1F2329" maxheight="22" autocalcwidth="true" />
+						<Control width="6" />
+						<Label text="采购 / 补货 / 紧急采购,提交后进入审核流程,按下单时间倒序" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" />
+						<Control />
+						<Label name="cg_rec_count" text="共 0 项" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" />
+					</HorizontalLayout>
+					<Control height="14" />
+					<VerticalLayout name="cg_rec_tblcard" bkimage="file='kc_tblcard.png' corner='10,45,10,10'" childpadding="0">
+						<List name="cg_rec_list" height="263" vscrollbar="false" inset="1,1,1,10" bkcolor="#00000000" itemhotbkcolor="#FFF7FAF8" itemselectedbkcolor="#FFEAF5EE">
+							<ListHeader height="44" bkcolor="#00000000" bordercolor="#00000000" bordersize="0" />
+							<ListHeaderItem text="采购单号" width="140" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="下单时间" width="150" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="商品数" width="110" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="金额(元)" width="160" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="经手人" width="130" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="状态" width="135" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="操作" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+						</List>
+					</VerticalLayout>
+					<Label name="cg_rec_foot" text="已全部加载 · 共 0 条" font="12" textcolor="#FF8A9099" height="44" valign="center" align="center" bkcolor="#FFFAFBFC" />
+				</VerticalLayout>
+
+				<!-- 采购统计卡:表头行 + 按商品维度统计表(含金额占比条)+ 页脚 -->
+				<VerticalLayout name="cg_stat_body" visible="false" padding="0,0,28,0" bkimage="file='bb_card10.png' corner='10,10,10,10'" inset="22,18,22,18" childpadding="0">
+					<HorizontalLayout height="22" childvalign="vcenter">
+						<Control width="3" maxheight="15" bkcolor="#FF2E9B5C" borderround="3,3" />
+						<Control width="10" />
+						<Label text="采购统计" font="116" textcolor="#FF1F2329" maxheight="22" autocalcwidth="true" />
+						<Control width="6" />
+						<Label text="按商品维度汇总本期采购数据,占比 = 该商品金额 / 采购总金额" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" />
+						<Control />
+						<Label name="cg_stat_count" text="共 0 项" font="12" textcolor="#FF8A9099" maxheight="17" autocalcwidth="true" />
+					</HorizontalLayout>
+					<Control height="14" />
+					<VerticalLayout name="cg_stat_tblcard" bkimage="file='kc_tblcard.png' corner='10,45,10,10'" childpadding="0">
+						<List name="cg_stat_list" height="263" vscrollbar="false" inset="1,1,1,10" bkcolor="#00000000" itemhotbkcolor="#FFF7FAF8" itemselectedbkcolor="#FFEAF5EE">
+							<ListHeader height="44" bkcolor="#00000000" bordercolor="#00000000" bordersize="0" />
+							<ListHeaderItem text="商品名称" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="分类" width="119" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="单位" width="96" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="采购单数" width="119" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="采购数量" width="143" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="金额(元)" width="143" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="金额占比" width="358" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+						</List>
+					</VerticalLayout>
+					<Label name="cg_stat_foot" text="已全部加载 · 共 0 项" font="12" textcolor="#FF8A9099" height="44" valign="center" align="center" bkcolor="#FFFAFBFC" />
+				</VerticalLayout>
+
+			</VerticalLayout>
+
+			<!-- ==================== 新建采购单 独立整页(挂 cg_page,与 cg_scroll 互斥显隐) ==================== -->
+			<VerticalLayout name="cg_view_order_new" visible="false" childpadding="0">
+				<HorizontalLayout name="cg_cgn_bar" height="64" bkcolor="#FFFFFFFF" bordersize="0,0,0,1" bordercolor="#FFE8EAED" inset="24,0,24,0" childpadding="14" childvalign="vcenter">
+					<Button name="cg_cgn_back" text="← 返回" font="14" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="78" maxheight="48" normalimage="file='gd_pill48_ghost_n.png' corner='24,24,24,24'" hotimage="file='gd_pill48_ghost_h.png' corner='24,24,24,24'" pushedimage="file='gd_pill48_ghost_p.png' corner='24,24,24,24'" />
+					<Label text="新建采购单" font="118" textcolor="#FF1F2329" maxheight="26" autocalcwidth="true" />
+					<Label text="提交后进入审核流程,审核通过才计入库存" font="12" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+					<Control />
+				</HorizontalLayout>
+				<VerticalLayout name="cg_cgn_body" vscrollbar="true" bkcolor="#FFF5F7FA" inset="24,14,0,18" childpadding="0">
+					<!-- 表单卡:R73 起仅「备注」一字段 -->
+					<VerticalLayout height="98" padding="0,0,24,0" bkimage="file='bb_card10.png' corner='10,10,10,10'" inset="20,14,20,14" childpadding="0">
+						<HorizontalLayout height="69" childpadding="12" childvalign="vcenter">
+							<VerticalLayout childpadding="7">
+								<Label text="备注" font="13" textcolor="#FF5F6368" height="18" />
+								<Edit name="cg_cgn_note" tipvalue="如「含配送费,送到后厨」" tipvaluecolor="#FFA8ADB3" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="12,0,12,0" height="44" maxheight="44" nativebkcolor="#FFFFFFFF" normalimage="file='input_r8_n.png' corner='8,8,8,8'" hotimage="file='input_r8_h.png' corner='8,8,8,8'" focusedimage="file='input_r8_f.png' corner='8,8,8,8'" />
+							</VerticalLayout>
+						</HorizontalLayout>
+					</VerticalLayout>
+					<Control height="12" padding="0,0,24,0" />
+					<HorizontalLayout height="70" padding="0,0,24,0" bkimage="file='bb_card10.png' corner='10,10,10,10'" inset="14,10,14,10" childpadding="12" childvalign="vcenter">
+						<Label name="cg_cgn_tip" text="已添加 0 个商品 · 点「添加商品」勾选更多,或直接扫条码追加;点数量 / 单价单元格调出数字键盘" font="13" textcolor="#FF5F6368" maxheight="20" autocalcwidth="true" />
+						<Control />
+						<Button name="cg_cgn_add" text="添加商品" font="114" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="102" maxheight="48" normalimage="file='btn_pill48_grn_n.png' corner='24,24,24,24'" hotimage="file='btn_pill48_grn_h.png' corner='24,24,24,24'" pushedimage="file='btn_pill48_grn_p.png' corner='24,24,24,24'" />
+					</HorizontalLayout>
+					<Control height="12" padding="0,0,24,0" />
+					<VerticalLayout name="cg_cgn_tblcard" height="223" padding="0,0,24,0" bkimage="file='kc_tblcard.png' corner='10,45,10,10'" childpadding="0">
+						<List name="cg_cgn_list" height="223" vscrollbar="false" inset="1,1,1,10" bkcolor="#00000000" itemhotbkcolor="#FFF7FAF8" itemselectedbkcolor="0">
+							<ListHeader height="44" bkcolor="#00000000" bordercolor="#00000000" bordersize="0" />
+							<ListHeaderItem text="商品名称" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="分类" width="149" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="单位" width="124" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+							<ListHeaderItem text="采购数量" width="187" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="单价(元)" width="174" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="金额(元)" width="174" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+							<ListHeaderItem text="操作" width="187" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+						</List>
+					</VerticalLayout>
+					<Control height="10" padding="0,0,24,0" />
+					<HorizontalLayout height="48" padding="0,0,24,0" bkimage="file='kc_tot_bg.png' corner='8,8,8,8'" inset="16,0,16,0" childpadding="0" childvalign="vcenter">
+						<Label text="合计商品" font="13" textcolor="#FF5F6368" maxheight="22" autocalcwidth="true" />
+						<Control width="5" />
+						<Label name="cg_cgn_tot_goods" text="0" font="113" textcolor="#FF1F2329" maxheight="22" autocalcwidth="true" />
+						<Control width="26" />
+						<Label text="采购总数" font="13" textcolor="#FF5F6368" maxheight="22" autocalcwidth="true" />
+						<Control width="5" />
+						<Label name="cg_cgn_tot_qty" text="0" font="113" textcolor="#FF1F2329" maxheight="22" autocalcwidth="true" />
+						<Control />
+						<Label name="cg_cgn_tot_amt_lab" text="采购金额" font="13" textcolor="#FF5F6368" maxheight="22" autocalcwidth="true" />
+						<Control width="34" />
+						<Label name="cg_cgn_tot_amt" text="¥0.00" font="119" textcolor="#FF2E9B5C" maxheight="26" autocalcwidth="true" />
+					</HorizontalLayout>
+				</VerticalLayout>
+			</VerticalLayout>
+
+			<!-- ==================== 页面级数字键盘(p6) ==================== -->
+			<VerticalLayout name="cg_keypad" visible="false" height="236" bkcolor="#FFFFFFFF" bordersize="0,1,0,0" bordercolor="#FFE8EAED" inset="24,0,24,0" childpadding="0">
+				<HorizontalLayout name="cg_kp_pad" childpadding="0" childvalign="vcenter">
+					<VerticalLayout width="300" childpadding="0">
+						<Control />
+						<Label name="cg_kp_tip" text="正在编辑 · 数量清空自动归 0" font="13" textcolor="#FF5F6368" height="18" />
+						<Control height="8" />
+						<Label name="cg_kp_field" text="--" font="115" textcolor="#FF1F2329" height="22" />
+						<Control height="4" />
+						<HorizontalLayout height="46" childpadding="0" childvalign="bottom">
+							<Label name="cg_kp_value" text="0" font="136" textcolor="#FF60B177" maxheight="46" autocalcwidth="true" />
+							<Label name="cg_kp_unit" text="" font="16" textcolor="#FF8A9099" maxheight="22" autocalcwidth="true" />
+							<Control />
+						</HorizontalLayout>
+						<Control />
+					</VerticalLayout>
+					<Control width="28" />
+					<HorizontalLayout name="cg_kp_grid" childpadding="8">
+						<VerticalLayout childpadding="8" childvalign="vcenter">
+							<Button name="cg_kp_k1" text="1" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k4" text="4" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k7" text="7" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k0" text="0" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+						</VerticalLayout>
+						<VerticalLayout childpadding="8" childvalign="vcenter">
+							<Button name="cg_kp_k2" text="2" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k5" text="5" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k8" text="8" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k00" text="00" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+						</VerticalLayout>
+						<VerticalLayout childpadding="8" childvalign="vcenter">
+							<Button name="cg_kp_k3" text="3" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k6" text="6" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_k9" text="9" font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_kdot" text="." font="122" textcolor="#FF1F2329" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" height="48" maxheight="48" normalimage="file='kc_key_n.png' corner='9,9,9,9'" hotimage="file='kc_key_h.png' corner='9,9,9,9'" pushedimage="file='kc_key_p.png' corner='9,9,9,9'" />
+						</VerticalLayout>
+						<VerticalLayout childpadding="8" childvalign="vcenter">
+							<Button name="cg_kp_del" text="删除" font="115" textcolor="#FFE14B4A" hottextcolor="#FFE14B4A" pushedtextcolor="#FFE14B4A" height="48" maxheight="48" normalimage="file='kc_keydel_n.png' corner='9,9,9,9'" hotimage="file='kc_keydel_h.png' corner='9,9,9,9'" pushedimage="file='kc_keydel_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_clr" text="清除" font="15" textcolor="#FF5F6368" hottextcolor="#FF5F6368" pushedtextcolor="#FF5F6368" height="48" maxheight="48" normalimage="file='kc_keyfn_n.png' corner='9,9,9,9'" hotimage="file='kc_keyfn_h.png' corner='9,9,9,9'" pushedimage="file='kc_keyfn_p.png' corner='9,9,9,9'" />
+							<Button name="cg_kp_ok" text="关闭" font="116" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" height="104" maxheight="104" normalimage="file='kc_keyok_n.png' corner='9,9,9,9'" hotimage="file='kc_keyok_h.png' corner='9,9,9,9'" pushedimage="file='kc_keyok_p.png' corner='9,9,9,9'" />
+						</VerticalLayout>
+					</HorizontalLayout>
+				</HorizontalLayout>
+			</VerticalLayout>
+
+			<!-- ==================== 新建采购单 底栏(页面级 / 声明在 cg_keypad 之后) ==================== -->
+			<HorizontalLayout name="cg_new_foot" visible="false" height="72" bkcolor="#FFFFFFFF" bordersize="0,1,0,0" bordercolor="#FFE8EAED" inset="24,0,24,0" childpadding="12" childvalign="vcenter">
+				<Label name="cg_cgn_foot_tip" text="提示:数量必填(键盘清空自动归 0),有数量为 0 的行不能提交" font="12" textcolor="#FF8A9099" maxheight="20" autocalcwidth="true" />
+				<Control />
+				<Button name="cg_cgn_cancel" text="取消" font="114" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" width="74" maxheight="48" normalimage="file='gd_pill48_ghost_n.png' corner='24,24,24,24'" hotimage="file='gd_pill48_ghost_h.png' corner='24,24,24,24'" pushedimage="file='gd_pill48_ghost_p.png' corner='24,24,24,24'" />
+				<Button name="cg_cgn_save" text="提交采购单" font="114" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" width="128" maxheight="48" normalimage="file='btn_pill48_grn_n.png' corner='24,24,24,24'" hotimage="file='btn_pill48_grn_h.png' corner='24,24,24,24'" pushedimage="file='btn_pill48_grn_p.png' corner='24,24,24,24'" />
+			</HorizontalLayout>
+
+			<!-- ==================== 添加商品弹层(p5 / p7) ==================== -->
+			<VerticalLayout name="cg_pick_overlay" visible="false" bkcolor="#6B1F2329" childpadding="0" float="0,0,1,1">
+				<Control />
+				<HorizontalLayout height="560" childpadding="0">
+					<Control />
+					<VerticalLayout width="940" bkimage="file='bb_card10.png' corner='10,10,10,10'" childpadding="0">
+						<HorizontalLayout height="56" inset="20,0,20,0" childvalign="vcenter">
+							<Label text="添加商品" font="117" textcolor="#FF1F2329" maxheight="24" autocalcwidth="true" />
+							<Control width="12" />
+							<Label text="勾选后确定,追加到采购明细" font="12" textcolor="#FF8A9099" maxheight="18" autocalcwidth="true" />
+							<Control />
+							<Button name="cg_pick_close" text="关闭" font="13" textcolor="#FF5F6368" width="72" maxheight="34" normalimage="file='gd_btn_ghost_n.png' corner='8,8,8,8'" hotimage="file='gd_btn_ghost_h.png' corner='8,8,8,8'" pushedimage="file='gd_btn_ghost_p.png' corner='8,8,8,8'" />
+						</HorizontalLayout>
+						<Control height="1" bkcolor="#FFE8EAED" />
+						<HorizontalLayout inset="18,14,18,14" childpadding="14">
+							<VerticalLayout name="cg_pick_cat" width="168" bkcolor="#FFF7F8FA" borderround="16,16" inset="8,8,8,8" childpadding="2">
+								<Button name="cg_pick_cat_0" text="全部商品" font="113" textcolor="#FF2E9B5C" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF1B8A4C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="file='dc_pill_h.png' corner='11,11,11,11'" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_1" text="主食" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_2" text="主料" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_3" text="调料" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_4" text="蔬菜" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_5" text="肉类" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+								<Button name="cg_pick_cat_6" text="蛋类" font="13" textcolor="#FF5F6368" hottextcolor="#FF2E9B5C" pushedtextcolor="#FF2E9B5C" height="44" maxheight="44" align="left" textpadding="14,0,10,0" normalimage="" hotimage="file='dc_pill_h.png' corner='11,11,11,11'" pushedimage="file='dc_pill.png' corner='11,11,11,11'" />
+							</VerticalLayout>
+							<VerticalLayout childpadding="0">
+								<HorizontalLayout height="44" childpadding="0" childvalign="vcenter">
+									<HorizontalLayout width="460" bkimage="file='input_normal.png' corner='8,8,8,8'" inset="14,0,12,0" childpadding="8" childvalign="vcenter">
+										<Control bkimage="dc_ico_search16.png" width="16" maxheight="16" />
+										<Edit name="cg_pick_search" tooltip="搜索商品名称 / 拼音码 / 条码" font="13" textcolor="#FF1F2329" maxheight="24" bkcolor="#00000000" nativebkcolor="#FFFFFFFF" />
+									</HorizontalLayout>
+									<Control />
+									<Label name="cg_pick_selcnt" text="已选 0 项" font="113" textcolor="#FF2E9B5C" maxheight="20" autocalcwidth="true" />
+								</HorizontalLayout>
+								<Control height="10" />
+								<List name="cg_pick_list" vscrollbar="true" bkcolor="#FFFFFFFF" itemhotbkcolor="#FFF7FAF8" itemselectedbkcolor="#FFF7FAF8">
+									<ListHeader height="44" bkcolor="#FFF0F2F5" bordercolor="#FFE8EAED" bordersize="0,0,0,1" />
+									<ListHeaderItem text="商品" align="left" font="13" textcolor="#FF5F6368" textpadding="18,0,0,0" />
+									<ListHeaderItem text="分类 · 单位" width="200" align="left" font="13" textcolor="#FF5F6368" textpadding="10,0,0,0" />
+									<ListHeaderItem text="库存" width="140" align="right" font="13" textcolor="#FF5F6368" textpadding="0,0,22,0" />
+								</List>
+							</VerticalLayout>
+						</HorizontalLayout>
+						<Control height="1" bkcolor="#FFE8EAED" />
+						<HorizontalLayout height="72" inset="20,0,20,0" childvalign="vcenter">
+							<Label text="已存在明细中的商品会自动跳过" font="12" textcolor="#FF8A9099" maxheight="18" autocalcwidth="true" />
+							<Control />
+							<Button name="cg_pick_clear" text="清空" font="14" textcolor="#FF5F6368" width="96" maxheight="48" normalimage="file='gd_btn_ghost_n.png' corner='8,8,8,8'" hotimage="file='gd_btn_ghost_h.png' corner='8,8,8,8'" pushedimage="file='gd_btn_ghost_p.png' corner='8,8,8,8'" />
+							<Control width="12" />
+							<Button name="cg_pick_cancel" text="取消" font="14" textcolor="#FF5F6368" width="112" maxheight="48" normalimage="file='gd_btn_ghost_n.png' corner='8,8,8,8'" hotimage="file='gd_btn_ghost_h.png' corner='8,8,8,8'" pushedimage="file='gd_btn_ghost_p.png' corner='8,8,8,8'" />
+							<Control width="12" />
+							<Button name="cg_pick_ok" text="确定追加" font="14" textcolor="#FFFFFFFF" width="136" maxheight="48" normalimage="file='btn_green_zhengchang.png' corner='8,8,8,8'" hotimage="file='btn_green_xuanfu.png' corner='8,8,8,8'" pushedimage="file='btn_green_dianji.png' corner='8,8,8,8'" />
+						</HorizontalLayout>
+					</VerticalLayout>
+					<Control />
+				</HorizontalLayout>
+				<Control />
+			</VerticalLayout>
+		</VerticalLayout>
 	</CaigouPage>
-</Window>
+</Window>

+ 74 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_doc_detail_wnd.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window size="660,626" caption="0,0,0,56" roundcorner="10,10" shadowsize="3" showshadow="true">
+	<!-- 采购订单详情弹框(设计稿)。同一张皮肤,标题 / 单号 / 状态标签 / 合计文案由 C++ 按单据填。
+	     窗高 = 366 + 52*明细行数,运行时按实际行数 SetWindowPos + CenterWindow(见 CCaigouDetailWnd::SetWindowHeight)。
+	     分段:kd_head 56 / kd_body inset 20,16,20,18 / kd_foot 68。
+	     kd_info 104(两行,每行 3 格:行1 采购单号/状态/经手人;行2 下单时间/商品数/备注,R73 删供应商与期望到货)。 -->
+	<VerticalLayout bkcolor="#FFFFFFFF">
+		<HorizontalLayout name="kd_head" height="56" inset="20,0,20,0" childpadding="12" childvalign="vcenter" bordersize="0,0,0,1" bordercolor="#FFE8EAED">
+			<Label name="kd_title" text="采购订单详情" font="116" textcolor="#FF1F2329" maxheight="24" valign="vcenter" autocalcwidth="true" />
+			<Label name="kd_sub" text="采购单详情" font="12" textcolor="#FF8A9099" maxheight="20" valign="vcenter" autocalcwidth="true" />
+			<Control />
+			<Button name="kd_close" width="28" maxheight="28" normalimage="file='kc_dlg_close_n.png'" hotimage="file='kc_dlg_close_h.png'" pushedimage="file='kc_dlg_close_p.png'" />
+		</HorizontalLayout>
+		<VerticalLayout name="kd_body" inset="20,16,20,18">
+			<VerticalLayout name="kd_info" height="104">
+				<HorizontalLayout name="kd_inforow1" height="46" childpadding="16">
+					<VerticalLayout name="kd_c1" width="196">
+						<Label name="kd_l1" text="采购单号" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v1" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+					<VerticalLayout name="kd_c2" width="196">
+						<Label name="kd_l2" text="状态" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v2" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+					<VerticalLayout name="kd_c3" width="196">
+						<Label name="kd_l3" text="经手人" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v3" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+				</HorizontalLayout>
+				<Control height="12" />
+				<HorizontalLayout name="kd_inforow2" height="46" childpadding="16">
+					<VerticalLayout name="kd_c4" width="196">
+						<Label name="kd_l4" text="下单时间" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v4" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+					<VerticalLayout name="kd_c5" width="196">
+						<Label name="kd_l5" text="商品数" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v5" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+					<VerticalLayout name="kd_c6" width="196">
+						<Label name="kd_l6" text="备注" height="16" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" />
+						<Control height="6" />
+						<Label name="kd_v6" text="--" height="24" font="113" textcolor="#FF1F2329" align="left" valign="vcenter" maxheight="24" maxwidth="196" autocalcwidth="true" />
+					</VerticalLayout>
+				</HorizontalLayout>
+			</VerticalLayout>
+			<Control height="14" />
+			<VerticalLayout name="kd_tblcard" height="315" bkimage="file='kc_tblcard.png' corner='10,45,10,10'">
+				<List name="kd_list" inset="1,1,1,10" vscrollbar="false" bkcolor="#00000000" itemhotbkcolor="#00000000" itemselectedbkcolor="#00000000">
+					<ListHeader height="44" bkcolor="#00000000" bordercolor="#00000000" bordersize="0" />
+					<ListHeaderItem text="商品名称" width="210" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+					<ListHeaderItem text="单位" width="74" align="left" font="113" textcolor="#FF5F6368" textpadding="14,0,0,0" />
+					<ListHeaderItem text="数量" width="99" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+					<ListHeaderItem text="单价(元)" width="111" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+					<ListHeaderItem text="金额(元)" width="124" align="right" font="113" textcolor="#FF5F6368" textpadding="0,0,14,0" />
+				</List>
+			</VerticalLayout>
+			<Control height="10" />
+			<HorizontalLayout name="kd_sum" height="25" childpadding="10" childalign="right" childvalign="vcenter">
+				<Label name="kd_sum_lab" text="商品 0 种 · 合计" font="13" textcolor="#FF5F6368" maxheight="22" valign="vcenter" autocalcwidth="true" />
+				<Label name="kd_sum_amt" text="¥0.00" font="116" textcolor="#FF2E9B5C" maxheight="25" valign="vcenter" autocalcwidth="true" />
+			</HorizontalLayout>
+		</VerticalLayout>
+		<HorizontalLayout name="kd_foot" height="68" inset="20,0,20,0" childpadding="12" childalign="right" childvalign="vcenter" bkcolor="#FFFAFBFC" bordersize="0,1,0,0" bordercolor="#FFE8EAED">
+			<Button name="kd_btn_close" text="关闭" width="65" maxheight="40" font="113" align="center" valign="vcenter" textcolor="#FF5F6368" hottextcolor="#FF1F2329" pushedtextcolor="#FF1F2329" normalimage="file='kc_btn_ghost_n.png' corner='20,20,20,20'" hotimage="file='kc_btn_ghost_h.png' corner='20,20,20,20'" pushedimage="file='kc_btn_ghost_p.png' corner='20,20,20,20'" />
+			<Button name="kd_btn_print" text="打印小票" width="103" maxheight="40" font="113" align="left" valign="vcenter" textpadding="31,0,0,0" textcolor="#FFFFFFFF" hottextcolor="#FFFFFFFF" pushedtextcolor="#FFFFFFFF" normalimage="file='kc_btn_primary_n.png' corner='30,20,20,20'" hotimage="file='kc_btn_primary_h.png' corner='30,20,20,20'" pushedimage="file='kc_btn_primary_p.png' corner='30,20,20,20'" />
+		</HorizontalLayout>
+	</VerticalLayout>
+</Window>

+ 16 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_row_doc_detail.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window>
+	<!-- 采购订单详情弹框 · 明细行(对齐 cg_doc_detail_wnd 的 kd_list 表头):
+	     商品名称(210) / 单位(74) / 数量(99) / 单价(元)(111) / 金额(元)(124)。
+	     列控件必须是 ListContainerElement 的直接子控件、且数量与表头列数一一对应 —— 框架会把
+	     每个子控件的左右边界强制覆盖成对应表头项的矩形
+	     (DuiLib/Control/UIList.cpp 的 CListContainerElementUI::SetPos);多一个子控件会撞 pHeaderItem==NULL 提前 return。
+	     行高 52,行下边框 1px;最后一行也带,由 kc_tblcard.png 圆角底边兜住。 -->
+	<ListContainerElement height="52" inset="0" childvalign="vcenter" bordersize="0,0,0,1" bordercolor="#FFE8EAED">
+		<Label name="col_name" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_unit" font="13" textcolor="#FF5F6368" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_qty" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_price" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_amount" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+	</ListContainerElement>
+</Window>

+ 19 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_row_edit.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window>
+	<!-- 新建采购单 · 明细行(设计稿)。列顺序:name flex / cat 149 / unit 124 / qty 187 / price 174 / amount 174 / op 187。可编辑单元格是 Button,底图走 input_r8_*;控件名由 C++ 重命名为 cg_cgn_qty_<行号> / cg_cgn_price_<行号> / cg_cgn_del_<行号>。 -->
+	<ListContainerElement height="56" inset="0" childvalign="vcenter" bordersize="0,0,0,1" bordercolor="#FFE8EAED">
+		<Label name="col_name" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_cat" width="149" font="13" textcolor="#FF5F6368" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_unit" width="124" font="13" textcolor="#FF5F6368" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<HorizontalLayout name="col_qty" width="187" inset="14,0,14,0" childvalign="vcenter">
+			<Button name="cell_qty" maxheight="44" font="14" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,12,0" normalimage="file='input_r8_n.png' corner='8,8,8,8'" hotimage="file='input_r8_h.png' corner='8,8,8,8'" pushedimage="file='input_r8_n.png' corner='8,8,8,8'" />
+		</HorizontalLayout>
+		<HorizontalLayout name="col_price" width="174" inset="14,0,14,0" childvalign="vcenter">
+			<Button name="cell_price" maxheight="44" font="14" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,12,0" normalimage="file='input_r8_n.png' corner='8,8,8,8'" hotimage="file='input_r8_h.png' corner='8,8,8,8'" pushedimage="file='input_r8_n.png' corner='8,8,8,8'" />
+		</HorizontalLayout>
+		<Label name="col_amount" width="174" font="13" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<HorizontalLayout name="col_op" width="187" inset="14,0,0,0" childvalign="vcenter">
+			<Button name="btn_del" text="删除" width="56" maxheight="28" font="13" textcolor="#FF2E9B5C" hottextcolor="#FF1B8A4C" pushedtextcolor="#FF1B8A4C" align="left" normalimage="" hotimage="" pushedimage="" />
+		</HorizontalLayout>
+	</ListContainerElement>
+</Window>

+ 12 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_row_pick.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window>
+	<!-- 添加商品弹层 · 商品行:整行可点(发 DUI_MSGTYPE_ITEMCLICK)。行根固定 name="pk_row";3 个直接子控件与 cg_pick_list 的 3 个表头列一一对应。勾选方块 pk_cb 是普通 Control,C++ 用 SetBkImage 在 kc_chk_off_24 / kc_chk_on_24 之间切(R73 真实对勾)。列宽:商品(flex) / 分类·单位200 / 库存140。 -->
+	<ListContainerElement name="pk_row" height="56" inset="0" childvalign="vcenter">
+		<HorizontalLayout name="col_name" inset="18,0,0,0" childpadding="12" childvalign="vcenter">
+			<Control name="pk_cb" bkimage="kc_chk_off_24.png" width="24" maxheight="24" />
+			<Label name="pk_name" font="14" textcolor="#FF1F2329" align="left" valign="vcenter" />
+		</HorizontalLayout>
+		<Label name="pk_meta" width="200" font="12" textcolor="#FF8A9099" align="left" valign="vcenter" textpadding="10,0,0,0" />
+		<Label name="pk_stock" width="140" font="13" textcolor="#FF5F6368" align="right" valign="vcenter" textpadding="0,0,22,0" />
+	</ListContainerElement>
+</Window>

+ 18 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_row_rec.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window>
+	<!-- 采购记录行:对齐 cg_rec_list 表头 —— 采购单号140 / 下单时间150 / 商品数110 / 金额(元)160 / 经手人130 / 状态135 / 操作(弹性)。状态是圆角胶囊,底图 / 文字色由 C++ 切换;列控件必须是 ListContainerElement 的直接子控件。 -->
+	<ListContainerElement height="54" inset="0" childvalign="vcenter" bordersize="0,0,0,1" bordercolor="#FFE8EAED">
+		<Label name="col_no" width="140" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_time" width="150" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_goods" width="110" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_amount" width="160" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_handler" width="130" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<HorizontalLayout name="col_status" width="135" inset="14,0,0,0" childvalign="vcenter">
+			<Label name="tag_status" font="112" textcolor="#FFE08A17" maxheight="24" bkimage="file='kc_tag_org.png' corner='12,12,12,12'" textpadding="12,0,12,0" autocalcwidth="true" valign="vcenter" />
+		</HorizontalLayout>
+		<HorizontalLayout name="col_op" inset="14,0,0,0" childpadding="14" childvalign="vcenter">
+			<Button name="op_detail" text="详情" font="13" textcolor="#FF2E9B5C" hottextcolor="#FF1B8A4C" pushedtextcolor="#FF1B8A4C" normalimage="" hotimage="" pushedimage="" width="44" />
+			<Button name="op_print" text="打印" font="13" textcolor="#FF2E9B5C" hottextcolor="#FF1B8A4C" pushedtextcolor="#FF1B8A4C" normalimage="" hotimage="" pushedimage="" width="44" />
+		</HorizontalLayout>
+	</ListContainerElement>
+</Window>

+ 18 - 0
bin/Win32/Debug/zhipuzi_pos_windows/skin/cg_row_stat.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Window>
+	<!-- 采购统计行:对齐 cg_stat_list 表头 —— 商品名称(余量) / 分类119 / 单位96 / 采购单数119 / 采购数量143 / 金额(元)143 / 金额占比358。占比条 120x8 胶囊,填充宽度由 C++ 按占比设置。 -->
+	<ListContainerElement height="54" inset="0" childvalign="vcenter" bordersize="0,0,0,1" bordercolor="#FFE8EAED">
+		<Label name="col_name" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_cat" width="119" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_unit" width="96" font="13" textcolor="#FF1F2329" align="left" valign="vcenter" textpadding="14,0,0,0" />
+		<Label name="col_cnt" width="119" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_qty" width="143" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<Label name="col_amount" width="143" font="113" textcolor="#FF1F2329" align="right" valign="vcenter" textpadding="0,0,14,0" />
+		<HorizontalLayout name="col_ratio" width="358" inset="14,0,14,0" childpadding="8" childvalign="vcenter">
+			<HorizontalLayout name="bar_track" width="120" height="8" maxheight="8" bkimage="file='kc_pbar_track.png' corner='4,4,4,4'" childpadding="0">
+				<Control name="bar_fill" width="0" height="8" maxheight="8" bkimage="file='kc_pbar_grn.png' corner='4,4,4,4'" />
+			</HorizontalLayout>
+			<Label name="bar_pct" width="58" font="12" textcolor="#FF5F6368" align="left" valign="vcenter" />
+		</HorizontalLayout>
+	</ListContainerElement>
+</Window>

BIN
dll/debug/DuiLib_d.dll


+ 402 - 19
zhipuzi_pos_windows/page/CCaigouPageUI.cpp

@@ -1,68 +1,451 @@
 #include "pch.h"
 #include "CCaigouPageUI.h"
-
 #include "../control/ControlEx.h"
+#include "../wnd/CMainWnd.h"
+#include "../wnd/CKucunQtyGuardWnd.h"
 
-#include "../tool/CSqlite3.h"
+namespace
+{
+	// 从被点的子控件回溯到 ListContainerElement,读 col_no 标签拿采购单号
+	std::wstring CgRowNo(CControlUI* p)
+	{
+		while (p != NULL)
+		{
+			CListContainerElementUI* row = dynamic_cast<CListContainerElementUI*>(p);
+			if (row != NULL)
+			{
+				CLabelUI* lab = static_cast<CLabelUI*>(row->FindSubControl(L"col_no"));
+				if (lab != NULL) return std::wstring((LPCTSTR)lab->GetText());
+				return L"";
+			}
+			p = p->GetParent();
+		}
+		return L"";
+	}
+}
 
 CCaigouPageUI::CCaigouPageUI()
+	: m_pMain(NULL), m_pNew(NULL), m_pKpHost(NULL), m_pPickOverlay(NULL), m_pPickList(NULL),
+	  m_nPickCat(0), m_nSubView(-1), m_nPeriod(0), m_nPageWidth(0)
 {
+	// 弹层候选项来自商品档案,勾选态(on)跨分类保留
+	const std::vector<KcGoods>& all = KcAllGoods();
+	for (size_t i = 0; i < all.size(); i++)
+	{
+		CgPickItem it;
+		it.name = all[i].name;
+		it.cat = all[i].cat;
+		it.unit = all[i].unit;
+		it.stock = all[i].stock;
+		it.on = false;
+		m_pick.push_back(it);
+	}
 }
 
-
 CCaigouPageUI::~CCaigouPageUI()
 {
+	if (m_pMain) { delete m_pMain; m_pMain = NULL; }
+	if (m_pNew) { delete m_pNew; m_pNew = NULL; }
 }
 
 void CCaigouPageUI::InitControl()
 {
+	CPaintManagerUI* pm = GetManager();
+
+	// 主态视图(记录 + 统计 双卡,host = cg_scroll)
+	CContainerUI* hMain = static_cast<CContainerUI*>(FindSubControl(L"cg_scroll"));
+	m_pMain = new CCaigouMainView(pm, hMain);
+	m_pMain->OnInitControl();
 
+	// 新建采购单子视图(host = cg_view_order_new,键盘 host = cg_keypad)
+	m_pKpHost = static_cast<CContainerUI*>(FindSubControl(L"cg_keypad"));
+	CContainerUI* hNew = static_cast<CContainerUI*>(FindSubControl(L"cg_view_order_new"));
+	m_pNew = new CCaigouNewView(pm, hNew, m_pKpHost);
+	m_pNew->OnInitControl();
+
+	// 添加商品弹层
+	m_pPickOverlay = static_cast<CContainerUI*>(FindSubControl(L"cg_pick_overlay"));
+	m_pPickList = static_cast<CListUI*>(FindSubControl(L"cg_pick_list"));
+
+	ShowMain();
 }
 
-//初始化当前页面的展示,处理默认展示效果,在页面被加载的时候调用
 void CCaigouPageUI::InitShow()
 {
+	ShowMain();
+	if (m_pMain) m_pMain->InitShow();
+	SyncPeriodSeg();
+}
+
+// ---------------------------------------------------------------- 视图显隐
+void CCaigouPageUI::ShowMain()
+{
+	CControlUI* pScroll = FindSubControl(L"cg_scroll");
+	if (pScroll) pScroll->SetVisible(true);
+	CControlUI* pNew = FindSubControl(L"cg_view_order_new");
+	if (pNew) pNew->SetVisible(false);
+	CControlUI* pFoot = FindSubControl(L"cg_new_foot");
+	if (pFoot) pFoot->SetVisible(false);
+	if (m_pKpHost) m_pKpHost->SetVisible(false);
+	if (m_pPickOverlay) m_pPickOverlay->SetVisible(false);
+	if (m_pNew) m_pNew->CloseKeypad();
 
+	m_nSubView = -1;
+	KcSyncNativeEditsIn(this);
 }
 
-//处理按钮点击类事件
-void CCaigouPageUI::HandleClickMsg(TNotifyUI& msg)
+void CCaigouPageUI::ShowSubView(int idx)
 {
-	CDuiString name = msg.pSender->GetName();
+	if (idx != 0 || m_pNew == NULL) return;
+
+	CControlUI* pScroll = FindSubControl(L"cg_scroll");
+	if (pScroll) pScroll->SetVisible(false);
+	CControlUI* pNew = FindSubControl(L"cg_view_order_new");
+	if (pNew) pNew->SetVisible(true);
+	CControlUI* pFoot = FindSubControl(L"cg_new_foot");
+	if (pFoot) pFoot->SetVisible(true);
+	if (m_pKpHost) m_pKpHost->SetVisible(false);
+	if (m_pPickOverlay) m_pPickOverlay->SetVisible(false);
 
+	m_nSubView = 0;
+	m_pNew->InitShow();
+	KcSyncNativeEditsIn(this);
 }
 
-//处理option切换事件
-void CCaigouPageUI::HandleSelectChangeMsg(TNotifyUI& msg)
+void CCaigouPageUI::SyncPeriodSeg()
 {
-	CDuiString name = msg.pSender->GetName();
+	for (int i = 0; i < 3; i++)
+	{
+		LPCTSTR nm[3] = { L"cg_per_0", L"cg_per_1", L"cg_per_2" };
+		CCaigouViewBase::StylePeriodSeg(
+			static_cast<CButtonUI*>(FindSubControl(nm[i])), i == m_nPeriod);
+	}
+	CControlUI* pRange = FindSubControl(L"cg_per_range");
+	// ⭐【R75】cg_per_range 是 autocalcwidth(区间文案长度按周期变)⇒ 走 SetLabelText,
+	//   只 SetText 会被按旧宽度裁掉。口径与视图侧 CCaigouMainView::SetPeriod 一致。
+	if (pRange != NULL && m_pMain != NULL)
+		CCaigouViewBase::SetLabelText(pRange, CgPeriodRangeText(m_nPeriod).c_str());
+}
 
-	COptionUI* curOption = static_cast<COptionUI*>(msg.pSender);
-	std::wstring groupname = curOption->GetGroup();
+// ---------------------------------------------------------------- 添加商品弹层
+void CCaigouPageUI::ShowPick(bool bShow)
+{
+	if (m_pPickOverlay == NULL) return;
 
+	if (bShow)
+	{
+		if (m_pNew) m_pNew->CloseKeypad();
+		for (size_t i = 0; i < m_pick.size(); i++) m_pick[i].on = false;
+		m_strPickSearch.clear();
+		CEditUI* pSearch = static_cast<CEditUI*>(FindSubControl(L"cg_pick_search"));
+		if (pSearch) pSearch->SetText(L"");
+		PickSetCat(0);
+	}
+	m_pPickOverlay->SetVisible(bShow);
+	KcSyncNativeEditsIn(this);
 }
 
-//处理下拉框、radio的切换事件
-void CCaigouPageUI::HandleItemSelectMsg(TNotifyUI& msg)
+void CCaigouPageUI::PickSetCat(int idx)
+{
+	if (idx < 0 || idx > 6) idx = 0;
+	m_nPickCat = idx;
+
+	for (int i = 0; i < 7; i++)
+	{
+		CButtonUI* p = static_cast<CButtonUI*>(FindSubControl(
+			(std::wstring(L"cg_pick_cat_") + std::to_wstring(i)).c_str()));
+		if (p == NULL) continue;
+		bool act = (i == idx);
+		p->SetNormalImage(act ? L"file='dc_pill_h.png' corner='11,11,11,11'" : L"");
+		p->SetTextColor(act ? 0xFF2E9B5C : 0xFF5F6368);
+		p->SetHotTextColor(0xFF2E9B5C);
+		p->SetPushedTextColor(0xFF1B8A4C);
+		p->SetFont(act ? 113 : 13);
+	}
+	RefreshPickList();
+}
+
+void CCaigouPageUI::RefreshPickList()
+{
+	if (m_pPickList == NULL) return;
+	while (m_pPickList->GetCount() > 0) m_pPickList->RemoveAt(0);
+	m_pickView.clear();
+
+	std::wstring kw = m_strPickSearch;
+
+	for (int i = 0; i < (int)m_pick.size(); i++)
+	{
+		const CgPickItem& it = m_pick[i];
+		if (m_nPickCat > 0 && CG_CATS[m_nPickCat] != it.cat) continue;
+		if (!kw.empty())
+		{
+			KcGoods g;
+			std::wstring py = KcLookupGoods(it.name, g) ? g.py : L"";
+			if (it.name.find(kw) == std::wstring::npos &&
+				py.find(kw) == std::wstring::npos) continue;
+		}
+
+		CDialogBuilder builder;
+		CDialogBuilderCallbackEx cb;
+		CListContainerElementUI* row =
+			static_cast<CListContainerElementUI*>(builder.Create(L"cg_row_pick.xml", (UINT)0, &cb, GetManager()));
+		if (row == NULL) continue;
+
+		CLabelUI* pName = static_cast<CLabelUI*>(row->FindSubControl(L"pk_name"));
+		CLabelUI* pMeta = static_cast<CLabelUI*>(row->FindSubControl(L"pk_meta"));
+		CLabelUI* pStock = static_cast<CLabelUI*>(row->FindSubControl(L"pk_stock"));
+		CControlUI* pCb = row->FindSubControl(L"pk_cb");
+		if (pName) pName->SetText(it.name.c_str());
+		if (pMeta) pMeta->SetText((it.cat + L" · " + it.unit).c_str());
+		if (pStock) pStock->SetText(std::to_wstring(it.stock).c_str());
+		if (pCb) pCb->SetBkImage(it.on ? L"kc_chk_on_24.png" : L"kc_chk_off_24.png");
+
+		m_pPickList->Add(row);
+		m_pickView.push_back(i);
+	}
+	SyncPickSelCnt();
+}
+
+void CCaigouPageUI::TogglePickRow(int idx)
+{
+	if (idx < 0 || idx >= (int)m_pickView.size()) return;
+	int gi = m_pickView[idx];
+	m_pick[gi].on = !m_pick[gi].on;
+
+	CListContainerElementUI* row =
+		static_cast<CListContainerElementUI*>(m_pPickList->GetItemAt(idx));
+	if (row != NULL)
+	{
+		CControlUI* pCb = row->FindSubControl(L"pk_cb");
+		if (pCb) pCb->SetBkImage(m_pick[gi].on ? L"kc_chk_on_24.png" : L"kc_chk_off_24.png");
+	}
+	SyncPickSelCnt();
+}
+
+void CCaigouPageUI::SyncPickSelCnt()
+{
+	int n = 0;
+	for (size_t i = 0; i < m_pick.size(); i++) if (m_pick[i].on) n++;
+	CLabelUI* p = static_cast<CLabelUI*>(FindSubControl(L"cg_pick_selcnt"));
+	// ⭐【R75】cg_pick_selcnt 是 autocalcwidth(「已选 3 项」→「已选 12 项」位数会变)
+	//   ⇒ 走 SetLabelText,只 SetText 会被按旧宽度裁掉。
+	if (p != NULL)
+		CCaigouViewBase::SetLabelText(p, (L"已选 " + std::to_wstring(n) + L" 项").c_str());
+}
+
+std::vector<std::wstring> CCaigouPageUI::CollectPicked() const
+{
+	std::vector<std::wstring> out;
+	for (size_t i = 0; i < m_pick.size(); i++)
+		if (m_pick[i].on) out.push_back(m_pick[i].name);
+	return out;
+}
+
+void CCaigouPageUI::AppendPickedFromOverlay()
 {
+	std::vector<std::wstring> picked = CollectPicked();
+	if (m_pNew) m_pNew->AppendPicked(picked);
+	ShowPick(false);
+}
+
+// ---------------------------------------------------------------- 详情弹框
+void CCaigouPageUI::OpenDetail(const CgDocDetail& doc)
+{
+	if (m_is_show_modal_wnd) return;
+
+	CCaigouDetailWnd* pDlg = new CCaigouDetailWnd(doc);
+	m_is_show_modal_wnd = true;
+	m_curModalWnd = pDlg;
+
+	pDlg->Create(GetManager()->GetPaintWindow(), _T(""), UI_WNDSTYLE_DIALOG, WS_EX_WINDOWEDGE);
+	pDlg->SetIcon(IDI_ICON_DUILIB);
+	pDlg->LoadDetail();
+
+	if (m_pMainWnd != NULL) m_pMainWnd->ShowGlobalDim(true);
+
+	pDlg->CenterWindow();
+	pDlg->ShowModal();
+
+	if (m_pMainWnd != NULL) m_pMainWnd->ShowGlobalDim(false);
+
+	m_is_show_modal_wnd = false;
+	m_curModalWnd = NULL;
+
+	const bool bPrint = pDlg->IsPrintClicked();
+	delete pDlg;
+
+	if (bPrint && m_pMainWnd != NULL)
+		m_pMainWnd->ShowToast(L"打印小票:热敏小票本轮未接入");
+}
+
+// ---------------------------------------------------------------- 事件路由
+void CCaigouPageUI::HandleClickMsg(TNotifyUI& msg)
+{
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+
+	// 主态「新建采购单」按钮 → 进子视图
+	if (n == L"cg_rec_new_btn" || n == L"cg_stat_new_btn") { ShowSubView(0); return; }
+
+	// ---- 添加商品弹层(必须在子视图分支【之前】判定!)
+	//   弹层正是从「新建采购单」页点「添加商品」打开的,此时 m_nSubView==0。
+	//   若把弹层判定放到子视图分支之后,弹层内任何点击都会先落进
+	//   `if (m_nSubView == 0)` 那段被整段吞掉、转发给 m_pNew->OnClick
+	//   ⇒ 弹层里的行 / 分类 / 清空 / 取消 / 确定 全部点不动。
+	//   判定顺序与库存页 CKucunPageUI::HandleClickMsg 保持一致。
+	if (n == L"pk_row")
+	{
+		int idx = (m_pPickList != NULL) ? m_pPickList->GetItemIndex(msg.pSender) : -1;
+		TogglePickRow(idx);
+		return;
+	}
+	if (n == L"cg_pick_close" || n == L"cg_pick_cancel") { ShowPick(false); return; }
+	if (n == L"cg_pick_clear")
+	{
+		for (size_t i = 0; i < m_pick.size(); i++) m_pick[i].on = false;
+		RefreshPickList();
+		return;
+	}
+	if (n == L"cg_pick_ok") { AppendPickedFromOverlay(); return; }
+	if (n.size() > 12 && n.compare(0, 12, L"cg_pick_cat_") == 0)
+	{
+		PickSetCat(_wtoi(n.c_str() + 12));
+		return;
+	}
+
+	// 新建态页面级按钮
+	if (m_nSubView == 0)
+	{
+		if (n == L"cg_cgn_back" || n == L"cg_cgn_cancel") { ShowMain(); return; }
+		if (n == L"cg_cgn_save")
+		{
+			// 【R74】保存 / 提交前的采购数量校验,口径与库存页入库 / 出库**完全一致**:
+			//   键盘清空即归 0、「不存在空」⇒ 0(含未录入)只能是漏填,硬拦不让走。
+			//   明细单元格边框早已按「编辑绿 / 0 琥珀 / 常态白」着色(ApplyCellStyle),
+			//   这里只负责把问题行摆到弹框里让用户看清是第几行、哪一项。
+			//   采购页只有一张明细表,无盘点语义 ⇒ kind 恒为 0、允许「删除该行」。
+			if (m_pNew != NULL)
+			{
+				std::vector<KcQtyIssue> issues;
+				m_pNew->CollectQtyIssues(issues);
+				if (!issues.empty())
+				{
+					// 逐行报「商品名 + 行号」:合计条只给项数,不报行号用户定位不到是哪一行。
+					// 弹框自己会截断到 8 行并追加「…等共 N 项」,这里不截。
+					std::vector<std::wstring> lines;
+					for (size_t i = 0; i < issues.size(); i++)
+					{
+						wchar_t b[256];
+						swprintf_s(b, L"%s(第 %d 行)· %s%s",
+							issues[i].name.c_str(), issues[i].row + 1,
+							issues[i].field.c_str(),
+							issues[i].unfilled ? L"未录入" : L"为 0");
+						lines.push_back(b);
+					}
+
+					// 「删除该行」只在**单行**出错时给 —— 多行时删哪一行有歧义。
+					bool bAllowDel = (issues.size() == 1);
+					CKucunQtyGuardWnd* pDlg = new CKucunQtyGuardWnd(0, lines, bAllowDel);
+					m_is_show_modal_wnd = true;
+					m_curModalWnd = pDlg;
 
+					pDlg->Create(GetManager()->GetPaintWindow(), _T(""), UI_WNDSTYLE_DIALOG, WS_EX_WINDOWEDGE);
+					pDlg->SetIcon(IDI_ICON_DUILIB);
+					pDlg->Load();              // 铺文案 + 按清单行数调窗高
+					if (m_pMainWnd != NULL) m_pMainWnd->ShowGlobalDim(true);
+					pDlg->CenterWindow();
+					pDlg->ShowModal();
+					if (m_pMainWnd != NULL) m_pMainWnd->ShowGlobalDim(false);
+					m_is_show_modal_wnd = false;
+					m_curModalWnd = NULL;
+
+					// 单行出错才点了就有反应(bAllowDel 已经保证多行时那枚按钮根本不显示,
+					// 这里再判一次 size()==1 是防呆):把它从明细里删掉,页面停在原处接着改。
+					bool bDel = pDlg->IsDeleteClicked();
+					delete pDlg;
+					if (bDel && issues.size() == 1) m_pNew->DeleteLine(issues[0].row);
+					return;                    // 硬拦:不保存、也不 leave 本页
+				}
+			}
+
+			if (m_pMainWnd != NULL) m_pMainWnd->ShowToast(L"采购单已提交(占位,后端未接入)");
+			return;
+		}
+		if (n == L"cg_cgn_add") { ShowPick(true); return; }
+		// 其余(明细行按钮、数字键盘)交给新建视图
+		if (m_pNew) m_pNew->OnClick(msg);
+		return;
+	}
+
+	// 记录行详情 / 打印
+	if (n == L"op_detail")
+	{
+		std::wstring no = CgRowNo(msg.pSender);
+		CgDocDetail d;
+		if (!CgLookupDetail(no, d))
+		{
+			if (m_pMainWnd != NULL) m_pMainWnd->ShowToast(L"未找到单据详情:" + no);
+			return;
+		}
+		OpenDetail(d);
+		return;
+	}
+	if (n == L"op_print")
+	{
+		std::wstring no = CgRowNo(msg.pSender);
+		if (m_pMainWnd != NULL)
+			m_pMainWnd->ShowToast(L"打印(占位):" + (no.empty() ? std::wstring(L"未取到单号") : no));
+		return;
+	}
+
+	// 其余交给主态视图
+	if (m_pMain) m_pMain->OnClick(msg);
+}
+
+void CCaigouPageUI::HandleSelectChangeMsg(TNotifyUI& msg)
+{
+	// 采购页无 Option / CheckBox;弹层勾选方块是 Control,靠整行 itemclick 切换
+	(void)msg;
+}
+
+void CCaigouPageUI::HandleItemSelectMsg(TNotifyUI& msg)
+{
+	if (m_pMain) m_pMain->OnItemSelect(msg);
 }
 
-//处理编辑框输入内容改变事件
 void CCaigouPageUI::HandleTextChangedMsg(TNotifyUI& msg)
 {
-	CDuiString name = msg.pSender->GetName();
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+	if (n == L"cg_pick_search")
+	{
+		CEditUI* pEdit = static_cast<CEditUI*>(msg.pSender);
+		if (pEdit) m_strPickSearch = (LPCTSTR)pEdit->GetText();
+		RefreshPickList();
+		return;
+	}
+	if (m_pMain) m_pMain->OnTextChanged(msg);
+}
+
+void CCaigouPageUI::HandleReturnMsg(TNotifyUI& msg)
+{
+	if (m_pMain) m_pMain->SubmitSearch();
+	(void)msg;
 }
 
-//处理扫码枪捕捉到的扫码信息
 void CCaigouPageUI::HandleTextCapture(std::string content)
 {
+	// 扫码枪:本轮不接后端,留空(将来可在新建态按条码追加商品)
+	(void)content;
+}
 
+bool CCaigouPageUI::HandleCustomMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+	// 采购页无自定义窗口消息需要处理
+	(void)uMsg; (void)wParam; (void)lParam;
+	return false;
 }
 
 void CCaigouPageUI::SetPos(RECT rc, bool bNeedInvalidate)
 {
 	m_nPageWidth = rc.right - rc.left;
-
 	CContainerUI::SetPos(rc, bNeedInvalidate);
-}
+}

+ 50 - 27
zhipuzi_pos_windows/page/CCaigouPageUI.h

@@ -1,51 +1,74 @@
 #pragma once
 
 #include "pch.h"
+#include <string>
+#include <vector>
 #include "../ui/CBasePageUI.h"
+#include "CCaigouViews.h"
 
-#include "../zhipuzi/CFood.h"
-#include "../zhipuzi/CFoodtype.h"
-#include "../zhipuzi/CFoodpackage.h"
-
-#include "../control/CDiandanFoodItemUI.h"
-#include "../zhipuzi/CDiandanOrder.h"
-
+// ============================================================================
+// 采购页宿主
+//
+// caigou_page.xml 一棵树里住着两类「页面」:
+//   主态    : cg_scroll(周期行 + 概览带 4 格 + 二级 ovseg(记录/统计) + 筛选行 + 记录卡 / 统计卡)
+//   子视图态: cg_view_order_new —— 设计稿新建采购单整页,用 SetVisible 与主态互斥(不注册新 Page)
+//   浮层    : cg_keypad(页面级数字键盘,新建页用)+ cg_pick_overlay(添加商品弹层)
+//
+// 与库存页同构:主态由 CCaigouMainView 管理(记录/统计双卡共用一棵 cg_scroll),
+// 新建态由 CCaigouNewView 管理(host = cg_view_order_new,键盘 host = cg_keypad)。
+// ============================================================================
 class CCaigouPageUI : public CBasePageUI
 {
 public:
 	CCaigouPageUI();
 	~CCaigouPageUI();
 
-	//初始化页面控件,页面创建构造的时候调用一次,注意和InitShow的区别,这个不触发控件的任何事件
 	void InitControl();
-
-	//初始化当前页面的展示,处理默认展示效果,在页面每次被选中加载(注意不是页面创建构造)的时候调用,如果多次选中会多次调用,这里要避免数据重复处理
 	void InitShow();
 
-	//处理按钮点击类事件
 	void HandleClickMsg(TNotifyUI& msg);
-
-	//处理option切换事件
 	void HandleSelectChangeMsg(TNotifyUI& msg);
-
-	//处理下拉框、radio的切换事件
 	void HandleItemSelectMsg(TNotifyUI& msg);
-
-	//处理编辑框输入内容改变事件
 	void HandleTextChangedMsg(TNotifyUI& msg);
-
-	//处理扫码枪捕捉到的扫码信息
+	void HandleReturnMsg(TNotifyUI& msg);
 	void HandleTextCapture(std::string content);
-
-	//处理自定义消息,各个页面处理各个页面自己的(是自己处理的消息返回true,不是自己处理的消息返回false)
-	bool HandleCustomMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
-	{
-		return false;
-	}
+	bool HandleCustomMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
 
 	void SetPos(RECT rc, bool bNeedInvalidate = true);
 
 private:
-	//当前控件的实际宽度
+	// 主态 / 子视图态 互斥显隐
+	void ShowMain();
+	void ShowSubView(int idx);            // 0 = 新建采购单
+
+	// 添加商品弹层(页面级,非模态)
+	void ShowPick(bool bShow);
+	void PickSetCat(int idx);
+	void RefreshPickList();
+	void TogglePickRow(int idx);
+	void SyncPickSelCnt();
+	std::vector<std::wstring> CollectPicked() const;
+	void AppendPickedFromOverlay();
+
+	// 采购订单详情弹框(真模态)
+	void OpenDetail(const CgDocDetail& doc);
+
+	// 周期分段控件(本月 / 上月 / 近三个月)—— 主态页级、由宿主统一刷
+	void SyncPeriodSeg();
+
+	CCaigouMainView* m_pMain;      // 主态视图(记录 + 统计)
+	CCaigouNewView*  m_pNew;       // 新建采购单子视图
+
+	CContainerUI* m_pKpHost;       // 页面级键盘面板宿主 cg_keypad
+	CContainerUI* m_pPickOverlay;  // 添加商品弹层
+	CListUI*      m_pPickList;     // 弹层列表
+
+	std::vector<CgPickItem> m_pick;   // 候选项(on 跨分类保留)
+	std::vector<int>        m_pickView;
+	int m_nPickCat;
+	std::wstring m_strPickSearch;
+
+	int m_nSubView;   // -1 = 主态;0 = 新建采购单
+	int m_nPeriod;    // 当前周期档(0 本月 / 1 上月 / 2 近三个月)
 	int m_nPageWidth;
-};
+};

+ 1198 - 0
zhipuzi_pos_windows/page/CCaigouViews.cpp

@@ -0,0 +1,1198 @@
+#include "pch.h"
+#include "CCaigouViews.h"
+#include "../control/ControlEx.h"
+
+// 分类下拉项(与商品档案 cat 取值一一对应;CG_CATS 在头文件以 extern 声明)
+extern const wchar_t* const CG_CATS[6] =
+{
+	L"主食", L"主料", L"调料", L"蔬菜", L"肉类", L"蛋类"
+};
+
+namespace
+{
+	// 采购记录(设计稿口径;以 2026-10-14 为今天:本月 3 / 上月 5 / 近三个月 8)
+	const CgRecItem g_rec[] = {
+		{ L"CG20261013001", L"2026-10-13 09:26", L"张丽", 5, 12860.00, CG_ST_AUDITED },
+		{ L"CG20261012003", L"2026-10-12 15:41", L"王强", 3,  5300.00, CG_ST_PENDING },
+		{ L"CG20261008002", L"2026-10-08 11:05", L"李明", 2,  1860.00, CG_ST_REJECTED },
+		{ L"CG20260928001", L"2026-09-28 10:12", L"张丽", 5, 12860.00, CG_ST_DONE },
+		{ L"CG20260927003", L"2026-09-27 16:38", L"王强", 3,  5300.00, CG_ST_AUDITED },
+		{ L"CG20260926002", L"2026-09-26 09:47", L"张丽", 2,   860.00, CG_ST_PENDING },
+		{ L"CG20260925006", L"2026-09-25 14:22", L"李明", 6, 15300.00, CG_ST_AUDITED },
+		{ L"CG20260924001", L"2026-09-24 11:30", L"王强", 4,  1600.00, CG_ST_DONE },
+		{ L"CG20260820003", L"2026-08-20 10:08", L"张丽", 3, 12400.00, CG_ST_AUDITED },
+	};
+	const int g_rec_n = (int)(sizeof(g_rec) / sizeof(g_rec[0]));
+
+	// 采购统计(按商品维度)
+	const CgStatItem g_stat[] = {
+		{ L"食用油", L"调料", L"桶", 4, 60,  3300.00 },
+		{ L"大米",   L"主食", L"斤", 3, 150, 1980.00 },
+		{ L"牛肉",   L"肉类", L"斤", 2, 80,  1600.00 },
+		{ L"面粉",   L"主料", L"袋", 2, 40,  1040.00 },
+		{ L"生抽",   L"调料", L"瓶", 1, 20,   500.00 },
+	};
+	const int g_stat_n = (int)(sizeof(g_stat) / sizeof(g_stat[0]));
+
+	// 采购订单详情明细(与记录表一一对应;每行 2 条明细)
+	const CgDetailRow g_d1[] = { { L"大米", L"斤", 100, 4.20, 420.00 }, { L"面粉", L"袋", 5, 26.00, 130.00 } };
+	const CgDetailRow g_d2[] = { { L"食用油", L"桶", 5, 55.00, 275.00 }, { L"牛肉", L"斤", 10, 28.50, 285.00 } };
+	const CgDetailRow g_d3[] = { { L"番茄", L"斤", 12, 4.50, 54.00 }, { L"鸡蛋", L"斤", 7, 6.00, 42.00 } };
+	const CgDetailRow g_d4[] = { { L"大米", L"斤", 100, 3.20, 320.00 }, { L"面粉", L"袋", 10, 32.00, 320.00 } };
+	const CgDetailRow g_d5[] = { { L"牛肉", L"斤", 30, 38.00, 1140.00 }, { L"食用油", L"桶", 5, 74.00, 370.00 } };
+	const CgDetailRow g_d6[] = { { L"大米", L"斤", 50, 3.20, 160.00 }, { L"面粉", L"袋", 4, 45.00, 180.00 } };
+	const CgDetailRow g_d7[] = { { L"食用油", L"桶", 10, 60.00, 600.00 }, { L"生抽", L"瓶", 20, 8.50, 170.00 } };
+	const CgDetailRow g_d8[] = { { L"大米", L"斤", 20, 3.20, 64.00 }, { L"面粉", L"袋", 2, 45.00, 90.00 } };
+	const CgDetailRow g_d9[] = { { L"牛肉", L"斤", 10, 38.00, 380.00 }, { L"鸡蛋", L"斤", 20, 6.00, 120.00 } };
+
+	struct CgDocRec
+	{
+		LPCTSTR no; int status; LPCTSTR handler, time; int goods; LPCTSTR note;
+		const CgDetailRow* rows; int n;
+	};
+	const CgDocRec g_docs[] = {
+		{ L"CG20261013001", CG_ST_AUDITED,  L"张丽", L"2026-10-13 09:26", 5, L"含配送费,送到后厨", g_d1, 2 },
+		{ L"CG20261012003", CG_ST_PENDING,  L"王强", L"2026-10-12 15:41", 3, L"", g_d2, 2 },
+		{ L"CG20261008002", CG_ST_REJECTED, L"李明", L"2026-10-08 11:05", 2, L"供应商缺货,已驳回", g_d3, 2 },
+		{ L"CG20260928001", CG_ST_DONE,     L"张丽", L"2026-09-28 10:12", 5, L"", g_d4, 2 },
+		{ L"CG20260927003", CG_ST_AUDITED,  L"王强", L"2026-09-27 16:38", 3, L"", g_d5, 2 },
+		{ L"CG20260926002", CG_ST_PENDING,  L"张丽", L"2026-09-26 09:47", 2, L"", g_d6, 2 },
+		{ L"CG20260925006", CG_ST_AUDITED,  L"李明", L"2026-09-25 14:22", 6, L"周末备货", g_d7, 2 },
+		{ L"CG20260924001", CG_ST_DONE,     L"王强", L"2026-09-24 11:30", 4, L"", g_d8, 2 },
+		{ L"CG20260820003", CG_ST_AUDITED,  L"张丽", L"2026-08-20 10:08", 3, L"", g_d9, 2 },
+	};
+	const int g_docs_n = (int)(sizeof(g_docs) / sizeof(g_docs[0]));
+
+	// 行元素回溯:从被点的子控件一路向上找到 ListContainerElement
+	CListContainerElementUI* CgFindRow(CControlUI* p)
+	{
+		while (p != NULL)
+		{
+			CListContainerElementUI* row = dynamic_cast<CListContainerElementUI*>(p);
+			if (row != NULL) return row;
+			p = p->GetParent();
+		}
+		return NULL;
+	}
+
+	// 整页滚动高度传播(参照 KcPropagateFixedHeightUp):写到 cg_scroll / cg_cgn_body 就停
+	static void CgPropagateFixedHeightUp(CControlUI* pFrom)
+	{
+		for (int guard = 0; pFrom != NULL && guard < 8; guard++)
+		{
+			CControlUI* p = pFrom->GetParent();
+			if (p == NULL) return;
+			CContainerUI* pC = dynamic_cast<CContainerUI*>(p);
+			if (pC == NULL) return;
+			LPCTSTR nm = pC->GetName();
+			if (nm != NULL && (_tcscmp(nm, _T("cg_scroll")) == 0 ||
+				_tcscmp(nm, _T("cg_cgn_body")) == 0)) return;
+
+			RECT rcIn = pC->GetInset();
+			int total = rcIn.top + rcIn.bottom;
+			int nVis = 0;
+			bool bOk = true;
+			for (int i = 0; i < pC->GetCount(); i++)
+			{
+				CControlUI* q = pC->GetItemAt(i);
+				if (q == NULL || !q->IsVisible() || q->IsFloat()) continue;
+				int ch = q->GetFixedHeight();
+				if (ch <= 0) ch = q->GetMaxHeight();
+				if (ch <= 0 || ch > 4000) { bOk = false; break; }
+				total += ch;
+				nVis++;
+			}
+			if (!bOk || nVis <= 0) return;
+			if (nVis > 1) total += pC->GetChildPadding() * (nVis - 1);
+			pC->SetFixedHeight(total);
+			pFrom = pC;
+		}
+	}
+}
+
+// ============================ 周期(自然月) ============================
+void CgPeriodBounds(int period, std::wstring& from, std::wstring& to)
+{
+	SYSTEMTIME st;
+	::GetLocalTime(&st);
+	const int y = st.wYear, m = st.wMonth;
+	int fromY = y, fromM = m;
+	if (period == 1) { fromM = m - 1; if (fromM < 1) { fromM = 12; fromY = y - 1; } }
+	else if (period == 2) { fromM = m - 2; while (fromM < 1) { fromM += 12; fromY--; } }
+	const int toY = (period == 2) ? y : fromY;
+	const int toM = (period == 2) ? m : fromM;
+
+	int dd = 31;
+	switch (toM)
+	{
+	case 2: dd = ((toY % 4 == 0 && toY % 100 != 0) || toY % 400 == 0) ? 29 : 28; break;
+	case 4: case 6: case 9: case 11: dd = 30; break;
+	default: dd = 31; break;
+	}
+	wchar_t b1[16], b2[16];
+	swprintf_s(b1, _countof(b1), L"%04d-%02d-01", fromY, fromM);
+	swprintf_s(b2, _countof(b2), L"%04d-%02d-%02d", toY, toM, dd);
+	from = b1;
+	to = b2;
+}
+
+bool CgInPeriod(const std::wstring& date, int period)
+{
+	if (date.size() < 10) return false;
+	std::wstring from, to;
+	CgPeriodBounds(period, from, to);
+	const std::wstring d = date.substr(0, 10);
+	return d >= from && d <= to;
+}
+
+std::wstring CgPeriodRangeText(int period)
+{
+	std::wstring from, to;
+	CgPeriodBounds(period, from, to);
+	return from + L" ~ " + to;
+}
+
+// ============================ 采购订单详情访问器 ============================
+bool CgLookupDetail(const std::wstring& no, CgDocDetail& out)
+{
+	for (int i = 0; i < g_docs_n; i++)
+	{
+		const CgDocRec& d = g_docs[i];
+		if (no != d.no) continue;
+		out.no = d.no;
+		out.status = d.status;
+		out.handler = d.handler;
+		out.time = d.time;
+		out.goods = d.goods;
+		out.note = d.note;
+		out.rows.clear();
+		for (int k = 0; k < d.n; k++) out.rows.push_back(d.rows[k]);
+		return true;
+	}
+	return false;
+}
+
+// ============================ CCaigouViewBase ============================
+CControlUI* CCaigouViewBase::Find(LPCTSTR name)
+{
+	if (m_pHost == NULL) return NULL;
+	return m_pHost->FindSubControl(name);
+}
+
+CListContainerElementUI* CCaigouViewBase::NewRow(LPCTSTR xmlfile)
+{
+	CDialogBuilder builder;
+	CDialogBuilderCallbackEx cb;
+	CControlUI* p = builder.Create(xmlfile, (UINT)0, &cb, m_pManager);
+	return static_cast<CListContainerElementUI*>(p);
+}
+
+void CCaigouViewBase::ClearList(CListUI* list)
+{
+	if (list == NULL) return;
+	while (list->GetCount() > 0) list->RemoveAt(0);
+}
+
+void CCaigouViewBase::SetRowText(CListContainerElementUI* row, LPCTSTR name, LPCTSTR text)
+{
+	if (row == NULL) return;
+	CLabelUI* p = static_cast<CLabelUI*>(row->FindSubControl(name));
+	if (p) p->SetText(text);
+}
+
+void CCaigouViewBase::SetRowColor(CListContainerElementUI* row, LPCTSTR name, DWORD color)
+{
+	if (row == NULL) return;
+	CLabelUI* p = static_cast<CLabelUI*>(row->FindSubControl(name));
+	if (p) p->SetTextColor(color);
+}
+
+void CCaigouViewBase::SetRowWidth(CListContainerElementUI* row, LPCTSTR name, int width)
+{
+	if (row == NULL) return;
+	CControlUI* p = row->FindSubControl(name);
+	if (p) p->SetFixedWidth(width);
+}
+
+void CCaigouViewBase::SetTag(CListContainerElementUI* row, LPCTSTR name, LPCTSTR text, int style)
+{
+	if (row == NULL) return;
+	CLabelUI* p = static_cast<CLabelUI*>(row->FindSubControl(name));
+	if (p == NULL) return;
+	KcApplyTag(p, std::wstring(text), style);   // 底图 + 文字色(共用库存页口径)
+	// ⭐【R75】cg_row_rec.xml 的 tag_status 是 autocalcwidth ⇒ 状态文案换长度时
+	//   只 SetText 会被按旧宽度裁掉(「待审核」↔「已驳回」长度虽同为 3 字,但换别的
+	//   文案就会露)。补 SetFont + NeedParentUpdate,机制见 SetLabelText 注释。
+	p->SetFont(p->GetFont());
+	p->NeedParentUpdate();
+}
+
+// ⭐【R75】给 autocalcwidth 标签写文本的唯一正确姿势(逐字照搬库存页 SetLabel / R48)。
+//   已在 DuiLib 源码坐实(UILabel.cpp):SetText 虽会 NeedParentUpdate,但不置
+//   m_bNeedEstimateSize ⇒ EstimateSize 走缓存、标签仍按旧文字宽度摆放、多出的字符被裁掉;
+//   只有 SetFont 会无条件置 m_bNeedEstimateSize,逼下一次布局按当前文字重新量宽。
+//   症状实例:键盘面板输入 555 只显示 5。
+void CCaigouViewBase::SetLabelText(CControlUI* p, LPCTSTR text, DWORD color)
+{
+	if (p == NULL) return;
+	CLabelUI* pl = static_cast<CLabelUI*>(p);
+	pl->SetText(text);
+	if (color != 0) pl->SetTextColor(color);
+	// ① SetFont 踢一脚 —— CLabelUI::SetFont 是唯一**无条件**置 m_bNeedEstimateSize 的
+	//    (UILabel.cpp:74-79,无早退判断),传当前字体号即可;
+	// ② NeedParentUpdate 让父布局重排一次,否则新文字仍按旧宽度摆放、会被裁掉。
+	pl->SetFont(pl->GetFont());
+	pl->NeedParentUpdate();
+}
+
+void CCaigouViewBase::SetLabel(LPCTSTR name, LPCTSTR text, DWORD color)
+{
+	SetLabelText(Find(name), text, color);
+}
+
+void CCaigouViewBase::SetStatInt(LPCTSTR name, int val, LPCWSTR unit)
+{
+	std::wstring s = KcGroupInt(std::to_wstring(val));
+	s += L"{x 4}{c #8A9099}{f 13}"; s += unit; s += L"{/f}{/c}";
+	SetLabel(name, s.c_str());          // 走 SetLabel:统计卡数字位数会变(0 → 12,345)
+}
+
+void CCaigouViewBase::SetStatMoney(LPCTSTR name, double val, LPCWSTR unit)
+{
+	std::wstring s = L"¥" + KcFormatMoney(val);
+	s += L"{x 4}{c #8A9099}{f 13}"; s += unit; s += L"{/f}{/c}";
+	SetLabel(name, s.c_str());          // 同上:金额位数会变
+}
+
+void CCaigouViewBase::SetListCount(LPCTSTR labelName, int n)
+{
+	std::wstring s = L"共 "; s += std::to_wstring(n); s += L" 项";
+	SetLabel(labelName, s.c_str());     // 同上:项数会变(共 3 项 → 共 128 项)
+}
+
+void CCaigouViewBase::FitListHeight(LPCTSTR listName, int nRows, int rowH)
+{
+	CListUI* pList = FindC<CListUI>(listName);
+	if (pList == NULL) return;
+	const int HEADER_H = 44;
+	int rows = (nRows > 0) ? nRows : 1;
+	RECT rcLi = pList->GetInset();
+	int h = rcLi.top + rcLi.bottom + HEADER_H + rows * rowH;
+	pList->SetFixedHeight(h);
+
+	int nCount = pList->GetCount();
+	if (nCount > 0)
+	{
+		CControlUI* pLast = pList->GetItemAt(nCount - 1);
+		if (pLast != NULL)
+		{
+			RECT rcNoBorder = { 0, 0, 0, 0 };
+			pLast->SetBorderSize(rcNoBorder);
+		}
+	}
+	CgPropagateFixedHeightUp(pList);
+}
+
+void CCaigouViewBase::AddComboItem(CComboUI* combo, LPCWSTR text)
+{
+	if (combo == NULL) return;
+	CListLabelElementUI* pElem = new CListLabelElementUI();
+	pElem->SetText(text);
+	combo->Add(pElem);
+}
+
+void CCaigouViewBase::SelectCombo(CComboUI* combo, int idx)
+{
+	if (combo == NULL) return;
+	combo->SelectItem(idx, false);
+}
+
+void CCaigouViewBase::ReadSearch(LPCTSTR editName, std::wstring& out)
+{
+	CEditUI* pEdit = FindC<CEditUI>(editName);
+	if (pEdit != NULL) out = (LPCTSTR)pEdit->GetText();
+}
+
+void CCaigouViewBase::SyncSearchTishi(LPCTSTR tishiName, LPCTSTR editName)
+{
+	CEditUI* pEdit = FindC<CEditUI>(editName);
+	if (pEdit == NULL) return;
+	CLabelUI* pTishi = FindC<CLabelUI>(tishiName);
+	if (pTishi == NULL) return;
+	pTishi->SetVisible(((LPCTSTR)pEdit->GetText())[0] == 0);
+}
+
+void CCaigouViewBase::StyleSeg(CButtonUI* p, bool act)
+{
+	if (p == NULL) return;
+	p->SetNormalImage(act ? L"file='kc_seggrn_n.png' corner='16,16,16,16'" : L"");
+	p->SetHotImage(act ? L"file='kc_seggrn_h.png' corner='16,16,16,16'"
+		: L"file='kc_seggray_h.png' corner='16,16,16,16'");
+	p->SetPushedImage(act ? L"file='kc_seggrn_p.png' corner='16,16,16,16'"
+		: L"file='kc_seggray_p.png' corner='16,16,16,16'");
+	p->SetFont(act ? 113 : 13);
+	p->SetTextColor(act ? 0xFFFFFFFF : 0xFF5F6368);
+	p->SetHotTextColor(act ? 0xFFFFFFFF : 0xFF2E9B5C);
+	p->SetPushedTextColor(act ? 0xFFFFFFFF : 0xFF2E9B5C);
+}
+
+void CCaigouViewBase::StylePeriodSeg(CButtonUI* p, bool act)
+{
+	if (p == NULL) return;
+	p->SetNormalImage(act ? L"file='kc_ovseg_on_n.png' corner='6,6,6,6'" : L"");
+	p->SetHotImage(act ? L"file='kc_ovseg_on_h.png' corner='6,6,6,6'"
+		: L"file='kc_ovseg_off_h.png' corner='6,6,6,6'");
+	p->SetPushedImage(act ? L"file='kc_ovseg_on_p.png' corner='6,6,6,6'"
+		: L"file='kc_ovseg_off_h.png' corner='6,6,6,6'");
+	p->SetTextColor(act ? 0xFF2E9B5C : 0xFF5F6368);
+	p->SetHotTextColor(0xFF2E9B5C);
+	p->SetPushedTextColor(0xFF2E9B5C);
+}
+
+// ============================ CCaigouMainView ============================
+void CCaigouMainView::OnInitControl()
+{
+	// 状态下拉:全部状态 / 待审核 / 已通过 / 已拒绝 / 已完成
+	CComboUI* pRec = FindC<CComboUI>(L"cg_rec_combo_status");
+	if (pRec)
+	{
+		AddComboItem(pRec, L"全部状态");
+		AddComboItem(pRec, L"待审核");
+		AddComboItem(pRec, L"已通过");
+		AddComboItem(pRec, L"已拒绝");
+		AddComboItem(pRec, L"已完成");
+		SelectCombo(pRec, 0);
+	}
+	// 统计态分类下拉(与添加商品同分类)
+	CComboUI* pStat = FindC<CComboUI>(L"cg_stat_combo_cat");
+	if (pStat)
+	{
+		AddComboItem(pStat, L"全部分类");
+		for (int i = 0; i < 6; i++) AddComboItem(pStat, CG_CATS[i]);
+		SelectCombo(pStat, 0);
+	}
+	SetSeg(0, false);
+}
+
+void CCaigouMainView::InitShow()
+{
+	RefreshOverview();
+	SetSeg(m_nSeg, true);
+}
+
+void CCaigouMainView::SetSeg(int active, bool bBuild)
+{
+	m_nSeg = (active == 1) ? 1 : 0;
+	CControlUI* pRec = Find(L"cg_rec_body");
+	CControlUI* pStat = Find(L"cg_stat_body");
+	CControlUI* pFrowRec = Find(L"cg_frow_rec");
+	CControlUI* pFrowStat = Find(L"cg_frow_stat");
+	if (pRec) pRec->SetVisible(m_nSeg == 0);
+	if (pStat) pStat->SetVisible(m_nSeg == 1);
+	if (pFrowRec) pFrowRec->SetVisible(m_nSeg == 0);
+	if (pFrowStat) pFrowStat->SetVisible(m_nSeg == 1);
+
+	StyleSeg(static_cast<CButtonUI*>(Find(L"cg_seg_rec")), m_nSeg == 0);
+	StyleSeg(static_cast<CButtonUI*>(Find(L"cg_seg_stat")), m_nSeg == 1);
+
+	if (bBuild)
+	{
+		if (m_nSeg == 0) BuildRecordRows();
+		else BuildStatRows();
+	}
+}
+
+void CCaigouMainView::SetPeriod(int period, bool bBuild)
+{
+	m_nPeriod = (period >= 0 && period <= 2) ? period : 0;
+	RefreshOverview();
+
+	// 周期分段控件 + 区间文案都在本视图 host(cg_scroll) 内,自己刷选中态与文案
+	static const LPCTSTR kNames[3] = { L"cg_per_0", L"cg_per_1", L"cg_per_2" };
+	for (int i = 0; i < 3; i++)
+		StylePeriodSeg(static_cast<CButtonUI*>(Find(kNames[i])), i == m_nPeriod);
+	CControlUI* pRange = Find(L"cg_per_range");
+	// ⭐【R75】cg_per_range 是 autocalcwidth,且区间文案长度按周期变
+	//   (「2026-08-17 ~ 2026-08-23」日 / 「2026-08-01 ~ 2026-08-31」月)⇒ 必须走 SetLabelText
+	SetLabelText(pRange, CgPeriodRangeText(m_nPeriod).c_str());
+
+	if (bBuild)
+	{
+		if (m_nSeg == 0) BuildRecordRows();
+		else BuildStatRows();
+	}
+}
+
+void CCaigouMainView::RefreshOverview()
+{
+	// 当前周期内全部记录(不过滤状态/搜索)算概览带
+	std::vector<CgRecItem> all;
+	for (int i = 0; i < g_rec_n; i++)
+	{
+		if (CgInPeriod(g_rec[i].date, m_nPeriod)) all.push_back(g_rec[i]);
+	}
+	int total = (int)all.size();
+	double amount = 0.0;
+	int audit = 0, reject = 0;
+	for (size_t i = 0; i < all.size(); i++)
+	{
+		amount += all[i].amount;
+		if (all[i].status == CG_ST_PENDING) audit++;
+		else if (all[i].status == CG_ST_REJECTED) reject++;
+	}
+	SetStatInt(L"cg_ov_total", total, L"单");
+	SetStatMoney(L"cg_ov_amount", amount, L"元");
+	SetStatInt(L"cg_ov_audit", audit, L"单");
+	SetStatInt(L"cg_ov_reject", reject, L"单");
+}
+
+void CCaigouMainView::BuildRecordRows()
+{
+	CListUI* pList = FindC<CListUI>(L"cg_rec_list");
+	if (pList == NULL) return;
+	ClearList(pList);
+	m_rec.clear();
+
+	// 【R76】记录卡**不**吃 m_strSearch:筛选行里唯一的搜索框
+	//   (cg_stat_search_edit「搜索商品名称/拼音码」)物理上属于统计卡(cg_frow_stat,
+	//   记录态 visible="false"),记录卡没有自己的搜索框。
+	//   原来这里拿 m_strSearch 去比 单号/经手人 ⇒ 在统计卡搜「米」再切到采购记录,
+	//   记录会被一个「商品名称」关键词按「单号/经手人」过滤掉,结果莫名其妙为空。
+	//   记录卡只按 周期 + 状态下拉 过滤。
+	for (int i = 0; i < g_rec_n; i++)
+	{
+		const CgRecItem& r = g_rec[i];
+		if (!CgInPeriod(r.date, m_nPeriod)) continue;
+		if (m_nStatus > 0 && r.status != (m_nStatus - 1)) continue;   // 1 待审核 ... 4 已完成
+		m_rec.push_back(r);
+
+		CListContainerElementUI* row = NewRow(L"cg_row_rec.xml");
+		if (row == NULL) continue;
+		SetRowText(row, L"col_no", r.no.c_str());
+		SetRowText(row, L"col_time", r.date.c_str());
+		SetRowText(row, L"col_goods", std::to_wstring(r.goods).c_str());
+		SetRowText(row, L"col_amount", (L"¥" + KcFormatMoney(r.amount)).c_str());
+		SetRowText(row, L"col_handler", r.handler.c_str());
+		SetTag(row, L"tag_status", CgStatusText(r.status), CgStatusStyle(r.status));
+		pList->Add(row);
+	}
+
+	SetListCount(L"cg_rec_count", (int)m_rec.size());
+	// 页脚:周期 + 已显示 / 总数
+	CLabelUI* pFoot = FindC<CLabelUI>(L"cg_rec_foot");
+	if (pFoot != NULL)
+	{
+		std::wstring s = CgPeriodRangeText(m_nPeriod) + L" · 已全部加载 · 共 " +
+			std::to_wstring((int)m_rec.size()) + L" 条";
+		pFoot->SetText(s.c_str());
+	}
+	FitListHeight(L"cg_rec_list", (int)m_rec.size(), 54);
+}
+
+void CCaigouMainView::BuildStatRows()
+{
+	CListUI* pList = FindC<CListUI>(L"cg_stat_list");
+	if (pList == NULL) return;
+	ClearList(pList);
+
+	// 占比分母 = 本期**采购总金额**,**不随筛选变**。
+	// 卡片副标题写死了口径「占比 = 该商品金额 / 采购总金额」,而「采购总金额」就是概览带
+	// 那一格(本期全量)。若改成按筛选后的子集当分母,同一行在筛选前后占比会跳变,
+	// 与副标题自相矛盾。所以这里只筛「展示哪些行」,分母恒用全量。
+	double total = 0.0;
+	for (int i = 0; i < g_stat_n; i++) total += g_stat[i].amount;
+
+	// 【R76】统计卡筛选:分类下拉 + 商品名称搜索。两者都只影响**展示哪些行**。
+	//   原来这里两个筛选都没接 —— 下拉选「主料」列表照样显示全部 5 行
+	//   (截图里 分类=主料 却列出 调料/主食/肉类/主料/调料,正是没生效的证据)。
+	const std::wstring kw = m_strSearch;
+
+	int shown = 0;
+	for (int i = 0; i < g_stat_n; i++)
+	{
+		const CgStatItem& s = g_stat[i];
+		// 分类筛选:0 = 全部分类;k>0 ⇒ CG_CATS[k-1](下拉第 0 项是「全部分类」)
+		if (m_nStatCat > 0 && s.cat != CG_CATS[m_nStatCat - 1]) continue;
+		// 商品名称搜索(搜索框提示语就是「搜索商品名称/拼音码」)
+		if (!kw.empty() && s.name.find(kw) == std::wstring::npos) continue;
+
+		CListContainerElementUI* row = NewRow(L"cg_row_stat.xml");
+		if (row == NULL) continue;
+		SetRowText(row, L"col_name", s.name.c_str());
+		SetRowText(row, L"col_cat", s.cat.c_str());
+		SetRowText(row, L"col_unit", s.unit.c_str());
+		SetRowText(row, L"col_cnt", std::to_wstring(s.orderCnt).c_str());
+		SetRowText(row, L"col_qty", std::to_wstring(s.qty).c_str());
+		SetRowText(row, L"col_amount", (L"¥" + KcFormatMoney(s.amount)).c_str());
+
+		// 占比条
+		int w = 0;
+		if (total > 1e-9) w = (int)(s.amount / total * 120.0 + 0.5);
+		if (w > 120) w = 120;
+		CControlUI* pFill = row->FindSubControl(L"bar_fill");
+		if (pFill) pFill->SetFixedWidth(w);
+		std::wstring pct = (total > 1e-9)
+			? (KcFormatMoney(s.amount / total * 100.0) + L"%") : L"0%";
+		SetRowText(row, L"bar_pct", pct.c_str());
+
+		pList->Add(row);
+		shown++;
+	}
+
+	// 计数 / 页脚 / 列表高度全部按**筛选后**的行数算(原来硬用 g_stat_n,筛选后会虚高)
+	SetListCount(L"cg_stat_count", shown);
+	SetLabel(L"cg_stat_foot",
+		(L"已全部加载 · 共 " + std::to_wstring(shown) + L" 项").c_str());
+	FitListHeight(L"cg_stat_list", shown, 54);
+}
+
+void CCaigouMainView::ResetFilter()
+{
+	// 【R76】「重置」必须把**两张卡**的筛选一起清干净,并且改的是**模型**(成员)而不只是控件。
+	//   原来只清了记录卡的状态下拉 + 统计卡的搜索框,**漏了统计卡的分类下拉** ⇒
+	//   用户选完「主料」点重置,下拉还停在「主料」(本轮报的 bug)。
+	//   另外原来也没同步 m_nStatCat,即便把下拉 UI 拨回 0,下次 BuildStatRows 仍按旧分类过滤。
+
+	// 记录卡:状态下拉
+	m_nStatus = 0;
+	// 统计卡:分类下拉
+	m_nStatCat = 0;
+	// 两个搜索框共用 m_strSearch(当前只有统计卡一个搜索框),一并清空
+	m_strSearch.clear();
+
+	CComboUI* pRec = FindC<CComboUI>(L"cg_rec_combo_status");
+	if (pRec) SelectCombo(pRec, 0);
+	CComboUI* pStat = FindC<CComboUI>(L"cg_stat_combo_cat");
+	if (pStat) SelectCombo(pStat, 0);            // ⭐ 分类下拉回到「全部分类」
+
+	CEditUI* pEdit = FindC<CEditUI>(L"cg_stat_search_edit");
+	if (pEdit) pEdit->SetText(L"");
+	SyncSearchTishi(L"cg_stat_search_tishi", L"cg_stat_search_edit");
+}
+
+void CCaigouMainView::ApplyRowFilter()
+{
+	// 真实接口前:内存已全量,重查即可(状态 / 分类 / 搜索都在 Build*Rows 内判)。
+	// ⭐【R76】要重刷**当前可见**的那张卡 —— 原来只无条件 BuildRecordRows(),
+	//   导致站在「采购统计」页点「查询 / 重置」时统计列表纹丝不动(下拉改了、行没变)。
+	//   切卡时 SetSeg() 会各自重建一次,所以这里只需管可见的那张。
+	if (m_nSeg == 0) BuildRecordRows();
+	else BuildStatRows();
+}
+
+void CCaigouMainView::OnClick(TNotifyUI& msg)
+{
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+	if (n == L"cg_seg_rec") { SetSeg(0); return; }
+	if (n == L"cg_seg_stat") { SetSeg(1); return; }
+	if (n == L"cg_per_0") { SetPeriod(0); return; }
+	if (n == L"cg_per_1") { SetPeriod(1); return; }
+	if (n == L"cg_per_2") { SetPeriod(2); return; }
+	if (n == L"cg_rec_new_btn") { /* 宿主拦截,进新建态 */ return; }
+	if (n == L"cg_stat_new_btn") { /* 宿主拦截 */ return; }
+	if (n == L"cg_stat_query_btn" || n == L"cg_stat_reset_btn")
+	{
+		if (n == L"cg_stat_reset_btn") ResetFilter();
+		ApplyRowFilter();
+		return;
+	}
+	if (n == L"op_detail" || n == L"op_print")
+	{
+		// 记录行操作:交给宿主开详情弹框(宿主能拿到 m_pMainWnd)
+		return;
+	}
+}
+
+void CCaigouMainView::OnItemSelect(TNotifyUI& msg)
+{
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+	if (n == L"cg_rec_combo_status")
+	{
+		CComboUI* p = static_cast<CComboUI*>(msg.pSender);
+		m_nStatus = p->GetCurSel();
+		BuildRecordRows();
+		return;
+	}
+	if (n == L"cg_stat_combo_cat")
+	{
+		// 【R76】统计分类筛选:先落到成员(m_nStatCat),再按它重建。
+		//   原来只 BuildStatRows() 不存状态 —— 于是「重置」就算把下拉拨回 0,
+		//   也没人能告诉 BuildStatRows 该按什么分类过滤。
+		//   与记录卡的状态下拉同策略:选完即生效(不要求再点「查询」)。
+		CComboUI* p = static_cast<CComboUI*>(msg.pSender);
+		m_nStatCat = p->GetCurSel();
+		BuildStatRows();
+		return;
+	}
+}
+
+void CCaigouMainView::OnTextChanged(TNotifyUI& msg)
+{
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+	if (n == L"cg_stat_search_edit")
+	{
+		ReadSearch(L"cg_stat_search_edit", m_strSearch);
+		SyncSearchTishi(L"cg_stat_search_tishi", L"cg_stat_search_edit");
+		return;
+	}
+}
+
+void CCaigouMainView::SubmitSearch()
+{
+	BuildRecordRows();
+}
+
+// ============================ CCaigouNewView(新建采购单) ============================
+void CCaigouNewView::OnInitControl()
+{
+	// 备注框是常驻原生 Edit,皮肤已铺好;此处无需额外初始化
+}
+
+void CCaigouNewView::InitShow()
+{
+	SeedLines();
+	BuildRows();
+	RefreshTot();
+}
+
+void CCaigouNewView::SeedLines()
+{
+	m_lines.clear();
+	// 初始塞 3 条样例商品(取自商品档案前 3 项)
+	const std::vector<KcGoods>& all = KcAllGoods();
+	for (int i = 0; i < 3 && i < (int)all.size(); i++)
+	{
+		CgEditLine ln;
+		ln.name = all[i].name;
+		ln.cat = all[i].cat;
+		ln.unit = all[i].unit;
+		ln.qty = L"1";
+		ln.price = KcFormatMoney(all[i].price);
+		m_lines.push_back(ln);
+	}
+}
+
+void CCaigouNewView::BuildRows()
+{
+	CListUI* pList = FindC<CListUI>(ListName());
+	if (pList == NULL) return;
+	ClearList(pList);
+	for (size_t i = 0; i < m_lines.size(); i++)
+	{
+		CListContainerElementUI* row = NewRow(RowXml());
+		if (row == NULL) continue;
+		// 单元格控件按行号重命名,便于键盘回填时精准定位(沿用 kc 的 <前缀>_<列>_<行> 约定)
+		CControlUI* pQty = row->FindSubControl(L"cell_qty");
+		CControlUI* pPrice = row->FindSubControl(L"cell_price");
+		CControlUI* pDel = row->FindSubControl(L"btn_del");
+		if (pQty) pQty->SetName((PrefixW() + L"_qty_" + std::to_wstring(i)).c_str());
+		if (pPrice) pPrice->SetName((PrefixW() + L"_price_" + std::to_wstring(i)).c_str());
+		if (pDel) pDel->SetName((PrefixW() + L"_del_" + std::to_wstring(i)).c_str());
+		pList->Add(row);
+	}
+	for (size_t i = 0; i < m_lines.size(); i++) RefreshRow((int)i);
+	SyncTableHeight();
+}
+
+void CCaigouNewView::RefreshRow(int i)
+{
+	if (i < 0 || i >= (int)m_lines.size()) return;
+	CListUI* pList = FindC<CListUI>(ListName());
+	if (pList == NULL) return;
+	CListContainerElementUI* row = static_cast<CListContainerElementUI*>(pList->GetItemAt(i));
+	if (row == NULL) return;
+	const CgEditLine& ln = m_lines[i];
+
+	SetRowText(row, L"col_name", ln.name.c_str());
+	SetRowText(row, L"col_cat", ln.cat.c_str());
+	SetRowText(row, L"col_unit", ln.unit.c_str());
+
+	CControlUI* pQty = row->FindSubControl((PrefixW() + L"_qty_" + std::to_wstring(i)).c_str());
+	CControlUI* pPrice = row->FindSubControl((PrefixW() + L"_price_" + std::to_wstring(i)).c_str());
+	if (pQty) pQty->SetText(ln.qty.c_str());
+	if (pPrice) pPrice->SetText(ln.price.c_str());
+
+	// 金额 = 数量 * 单价
+	double q = 0, p = 0;
+	q = _wtof(ln.qty.c_str());
+	p = _wtof(ln.price.c_str());
+	double amt = q * p;
+	SetRowText(row, L"col_amount", (L"¥" + KcFormatMoney(amt)).c_str());
+
+	// 边框色跟着数量走:编辑中绿 / 采购数量为 0 琥珀 / 常态白
+	ApplyCellStyle(i, 0);
+	ApplyCellStyle(i, 1);
+}
+
+void CCaigouNewView::RefreshTot()
+{
+	int goods = (int)m_lines.size();
+	int qty = 0;
+	double amt = 0.0;
+	for (size_t i = 0; i < m_lines.size(); i++)
+	{
+		qty += (int)(_wtof(m_lines[i].qty.c_str()) + 1e-9);
+		amt += _wtof(m_lines[i].qty.c_str()) * _wtof(m_lines[i].price.c_str());
+	}
+	// 合计条三格 + 提示行都是 autocalcwidth ⇒ 一律走 SetLabel(只 SetText 会被按旧宽度裁掉)
+	SetLabel(L"cg_cgn_tot_goods", std::to_wstring(goods).c_str());
+	SetLabel(L"cg_cgn_tot_qty", std::to_wstring(qty).c_str());
+	SetLabel(L"cg_cgn_tot_amt", (L"¥" + KcFormatMoney(amt)).c_str());
+	SetLabel(L"cg_cgn_tip",
+		(L"已添加 " + std::to_wstring(goods) + L" 个商品 · 点「添加商品」勾选更多,或直接扫条码追加").c_str());
+}
+
+void CCaigouNewView::SyncTableHeight()
+{
+	FitListHeight(ListName(), (int)m_lines.size(), 56);
+}
+
+void CCaigouNewView::DeleteLine(int idx)
+{
+	if (idx < 0 || idx >= (int)m_lines.size()) return;
+	m_lines.erase(m_lines.begin() + idx);
+	CloseKeypad();
+	BuildRows();
+	RefreshTot();
+}
+
+void CCaigouNewView::AppendPicked(const std::vector<std::wstring>& names)
+{
+	for (size_t k = 0; k < names.size(); k++)
+	{
+		// 已在明细里的去重
+		bool dup = false;
+		for (size_t j = 0; j < m_lines.size(); j++)
+		{
+			if (m_lines[j].name == names[k]) { dup = true; break; }
+		}
+		if (dup) continue;
+
+		KcGoods g;
+		CgEditLine ln;
+		ln.name = names[k];
+		if (KcLookupGoods(names[k], g))
+		{
+			ln.cat = g.cat;
+			ln.unit = g.unit;
+			ln.price = KcFormatMoney(g.price);
+		}
+		else
+		{
+			ln.cat = L""; ln.unit = L""; ln.price = L"0.00";
+		}
+		ln.qty = L"1";
+		m_lines.push_back(ln);
+	}
+	BuildRows();
+	RefreshTot();
+}
+
+// ---- 明细可编辑单元格的底图(三态:常态白 / 编辑中绿 / 采购数量为 0 琥珀)----
+// 全部复用现网资产:input_r8_*(常态)、btn_r8_green_*(编辑中)、btn_r8_org_*(数量为 0)。
+// 零新增美术资源;也绝不动全工程共用的 btn_r8_green/red_*。
+static const wchar_t* CG_CELL_N = L"file='input_r8_n.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_H = L"file='input_r8_h.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_P = L"file='input_r8_n.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_EN = L"file='btn_r8_green_n.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_EH = L"file='btn_r8_green_h.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_EP = L"file='btn_r8_green_p.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_ZN = L"file='btn_r8_org_n.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_ZH = L"file='btn_r8_org_h.png' corner='8,8,8,8'";
+static const wchar_t* CG_CELL_ZP = L"file='btn_r8_org_p.png' corner='8,8,8,8'";
+
+// 键盘面板 cg_keypad / 底栏 cg_new_foot 是 cg_view_order_new 的**兄弟**(同挂 cg_page),
+// 所以 cg_kp_* / cg_cgn_foot_* 这些控件不在本视图子树里。依次在
+// 本视图 → 键盘面板 → 键盘面板父级(cg_page)里查,才能找到(否则键盘数值显示不刷新)。
+CControlUI* CCaigouNewView::Find(LPCTSTR name)
+{
+	CControlUI* p = CCaigouViewBase::Find(name);
+	if (p == NULL && m_pKpHost != NULL) p = m_pKpHost->FindSubControl(name);
+	if (p == NULL && m_pKpHost != NULL)
+	{
+		CContainerUI* pPar = static_cast<CContainerUI*>(m_pKpHost->GetParent());
+		if (pPar != NULL) p = pPar->FindSubControl(name);
+	}
+	return p;
+}
+
+// 数量为 0 判定:空串**不算** 0(采购页不存在「未录」,空只在中间态出现)
+bool CCaigouNewView::IsQtyZero(int i) const
+{
+	if (i < 0 || i >= (int)m_lines.size()) return false;
+	const std::wstring& q = m_lines[i].qty;
+	if (q.empty()) return false;
+	return _wtof(q.c_str()) <= 0.0;
+}
+
+// 单元格底图 / 文字色:编辑中(绿) > 数量为 0(琥珀) > 常态(白)。
+void CCaigouNewView::ApplyCellStyle(int i, int col)
+{
+	if (i < 0 || i >= (int)m_lines.size()) return;
+	const std::wstring nm =
+		PrefixW() + ((col == 0) ? L"_qty_" : L"_price_") + std::to_wstring(i);
+	CButtonUI* p = FindC<CButtonUI>(nm.c_str());
+	if (p == NULL) return;
+
+	const bool bEditing = (m_nEditRow == i && m_nEditCol == col);
+	const bool bZero = (col == 0) && (!bEditing) && IsQtyZero(i);
+
+	const wchar_t* n = CG_CELL_N;
+	const wchar_t* h = CG_CELL_H;
+	const wchar_t* pu = CG_CELL_P;
+	DWORD tc = 0xFF1F2329;
+	if (bEditing) { n = CG_CELL_EN; h = CG_CELL_EH; pu = CG_CELL_EP; tc = 0xFF2E9B5C; }
+	else if (bZero) { n = CG_CELL_ZN; h = CG_CELL_ZH; pu = CG_CELL_ZP; tc = KC_COLOR_WARN; }
+
+	p->SetNormalImage(n);
+	p->SetHotImage(h);
+	p->SetPushedImage(pu);
+	p->SetTextColor(tc);
+}
+
+void CCaigouNewView::ApplyAllCellStyles()
+{
+	for (int i = 0; i < (int)m_lines.size(); i++)
+	{
+		ApplyCellStyle(i, 0);
+		ApplyCellStyle(i, 1);
+	}
+}
+
+// 「不存在空」:键盘清空 / 删空之后把缓冲补成 0(与入库同口径;采购页没有「未录」态)
+void CCaigouNewView::NormalizeBuf()
+{
+	if (m_editBuf.empty()) m_editBuf = L"0";
+}
+
+void CCaigouNewView::CloseKeypad()
+{
+	if (m_pKpHost != NULL) m_pKpHost->SetVisible(false);
+	m_nEditRow = -1;
+	m_nEditCol = 0;
+	m_editBuf.clear();
+	if (m_pKpHost != NULL) KcSyncNativeEditsIn(m_pKpHost);
+	KcSyncNativeEditsIn(m_pHost);
+	ApplyAllCellStyles();          // 收起后清掉「编辑中」绿框
+}
+
+void CCaigouNewView::OpenKeypad(int row, int col)
+{
+	if (row < 0 || row >= (int)m_lines.size()) return;
+	m_nEditRow = row;
+	m_nEditCol = col;
+	m_editBuf = (col == 0) ? m_lines[row].qty : m_lines[row].price;
+	NormalizeBuf();
+
+	if (m_pKpHost != NULL) m_pKpHost->SetVisible(true);
+	KeypadRefresh();
+	ApplyAllCellStyles();
+	// 键盘挤占主体高度 ⇒ 主体内的常驻原生 Edit(备注)要重新定位,否则会停在旧位置
+	KcSyncNativeEditsIn(m_pHost);
+}
+
+void CCaigouNewView::KeypadRefresh()
+{
+	// ⭐【R75】cg_kp_value / cg_kp_unit 都是 autocalcwidth 标签,写文本必须走 SetLabel
+	//   (SetText + SetFont(GetFont()) + NeedParentUpdate())。
+	//   只调 SetText 的话,标签宽度停留在上一次文字算出的值 ⇒ 输入 555 时宽度仍是 "5"
+	//   的宽度,后两位被裁掉(用户反馈「输入 555 只显示第一个数字」就是这个原因)。
+	SetLabel(L"cg_kp_field", m_nEditCol == 0 ? L"采购数量" : L"单价");
+	SetLabel(L"cg_kp_value", m_editBuf.empty() ? L"0" : m_editBuf.c_str());
+	if (m_nEditRow >= 0 && m_nEditRow < (int)m_lines.size())
+		SetLabel(L"cg_kp_unit", m_lines[m_nEditRow].unit.c_str());
+	else
+		SetLabel(L"cg_kp_unit", L"");
+}
+
+void CCaigouNewView::KpDigit(LPCTSTR t)
+{
+	if (m_nEditRow < 0) return;
+	if (m_editBuf == L"0") m_editBuf.clear();
+	if (m_editBuf.size() + wcslen(t) > 12) return;
+	m_editBuf += t;
+	KeypadRefresh();
+	KpApply();                 // 实时写回明细行(与入库同逻辑:每按一键即生效)
+}
+
+void CCaigouNewView::KpDot()
+{
+	if (m_nEditRow < 0) return;
+	if (m_nEditCol != 1) return;                       // 只有单价允许小数
+	if (m_editBuf.find(L".") != std::wstring::npos) return;
+	if (m_editBuf.empty()) m_editBuf = L"0";
+	m_editBuf += L".";
+	KeypadRefresh();
+	KpApply();
+}
+
+void CCaigouNewView::KpDel()
+{
+	if (m_nEditRow < 0) return;
+	if (!m_editBuf.empty()) m_editBuf.erase(m_editBuf.size() - 1);
+	NormalizeBuf();
+	KeypadRefresh();
+	KpApply();
+}
+
+void CCaigouNewView::KpClear()
+{
+	if (m_nEditRow < 0) return;
+	m_editBuf.clear();
+	NormalizeBuf();
+	KeypadRefresh();
+	KpApply();
+}
+
+// 把键盘缓冲**实时**写回明细:每按一键就生效,不等点「关闭」。
+// 写 m_lines → 刷本行单元格(含金额 / 边框色)→ 刷合计条。
+void CCaigouNewView::KpApply()
+{
+	if (m_nEditRow < 0 || m_nEditRow >= (int)m_lines.size()) return;
+	NormalizeBuf();
+	if (m_nEditCol == 0) m_lines[m_nEditRow].qty = m_editBuf;
+	else m_lines[m_nEditRow].price = m_editBuf;
+	RefreshRow(m_nEditRow);
+	RefreshTot();
+}
+
+void CCaigouNewView::KpCommit()
+{
+	KpApply();
+	CloseKeypad();
+}
+
+// 保存前数量校验(口径同入库):采集「采购数量为空 / ≤ 0」的行
+void CCaigouNewView::CollectQtyIssues(std::vector<KcQtyIssue>& out) const
+{
+	for (int i = 0; i < (int)m_lines.size(); i++)
+	{
+		const std::wstring& q = m_lines[i].qty;
+		if (!q.empty() && _wtof(q.c_str()) > 0.0) continue;
+
+		KcQtyIssue it;
+		it.row = i;
+		it.name = m_lines[i].name;
+		it.field = L"采购数量";
+		it.unfilled = false;
+		out.push_back(it);
+	}
+}
+
+void CCaigouNewView::OnClick(TNotifyUI& msg)
+{
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+
+	// 数字键盘
+	if (n == L"cg_kp_ok") { KpCommit(); return; }
+	if (n == L"cg_kp_del") { KpDel(); return; }
+	if (n == L"cg_kp_clr") { KpClear(); return; }
+	if (n == L"cg_kp_kdot") { KpDot(); return; }
+	if (n.size() >= 7 && n.compare(0, 7, L"cg_kp_k") == 0)
+	{
+		// cg_kp_k1..k9 / cg_kp_k0 / cg_kp_k00:数字来自按钮文本
+		// ("_kdot" 已在上面精确匹配并 return,不会误入这里)
+		std::wstring d = (LPCTSTR)msg.pSender->GetText();
+		if (!d.empty()) KpDigit(d.c_str());
+		return;
+	}
+
+	// 明细行:数量 / 单价单元格 → 数字键盘;行尾删除按钮 → 删行。
+	// 判定用 tail = 控件名去掉 <前缀> 之后的部分(形如 "_qty_3" / "_price_3" / "_del_3"),
+	// 不要用「前缀长度 + 常数」硬算 substr 长度 —— _qty_ / _del_ 是 5 字符、_price_ 是 7 字符,
+	// 常数写错一位就会让匹配恒失败(表现为「点数量和单价单元格没反应」)。
+	// 与库存页 CKucunEditViewBase::OnClick 同法。
+	CListContainerElementUI* row = CgFindRow(msg.pSender);
+	if (row != NULL)
+	{
+		CListUI* pList = FindC<CListUI>(ListName());
+		int idx = (pList != NULL) ? pList->GetItemIndex(row) : -1;
+		if (idx >= 0)
+		{
+			const std::wstring pfx = PrefixW();
+			const std::wstring tail =
+				(n.size() > pfx.size()) ? n.substr(pfx.size()) : std::wstring();
+			if (tail.compare(0, 5, L"_qty_") == 0) { OpenKeypad(idx, 0); return; }
+			if (tail.compare(0, 7, L"_price_") == 0) { OpenKeypad(idx, 1); return; }
+			if (tail.compare(0, 5, L"_del_") == 0) { DeleteLine(idx); return; }
+		}
+	}
+}
+
+// ============================ CCaigouDetailWnd ============================
+namespace
+{
+	CLabelUI* CgDFind(CPaintManagerUI& pm, LPCTSTR name)
+	{
+		return static_cast<CLabelUI*>(pm.FindControl(name));
+	}
+	void CgDSet(CPaintManagerUI& pm, LPCTSTR name, const std::wstring& text)
+	{
+		CLabelUI* p = CgDFind(pm, name);
+		if (p == NULL) return;
+		p->SetText(text.c_str());
+		// ⭐【R75】cg_doc_detail_wnd.xml 的 kd_title / kd_sub / kd_v1~v6 / kd_sum_amt 全是
+		//   autocalcwidth ⇒ 只 SetText 会被按旧宽度裁掉(单号/金额位数一变就露)。
+		//   与 CgDValue 同口径补 SetFont + NeedParentUpdate。
+		p->SetFont(p->GetFont());
+		p->NeedParentUpdate();
+	}
+	void CgDValue(CPaintManagerUI& pm, LPCTSTR name, const std::wstring& text, int tag)
+	{
+		CLabelUI* p = CgDFind(pm, name);
+		if (p == NULL) return;
+		if (tag >= 0) KcApplyTag(p, text, tag);
+		else { p->SetBkImage(L""); p->SetText(text.c_str()); p->SetTextColor(0xFF1F2329); }
+		p->SetFont(p->GetFont());
+	}
+	CListContainerElementUI* CgDNewRow(CPaintManagerUI* pm, LPCTSTR xmlfile)
+	{
+		CDialogBuilder builder;
+		CDialogBuilderCallbackEx cb;
+		CControlUI* p = builder.Create(xmlfile, (UINT)0, &cb, pm);
+		return static_cast<CListContainerElementUI*>(p);
+	}
+}
+
+LRESULT CCaigouDetailWnd::OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+{
+	POINT pt; pt.x = GET_X_LPARAM(lParam); pt.y = GET_Y_LPARAM(lParam);
+	::ScreenToClient(*this, &pt);
+	RECT rcClient; ::GetClientRect(*this, &rcClient);
+	RECT rcCaption = m_pm.GetCaptionRect();
+	if (pt.x >= rcClient.left + rcCaption.left && pt.x < rcClient.right - rcCaption.right
+		&& pt.y >= rcCaption.top && pt.y < rcCaption.bottom)
+	{
+		CControlUI* pControl = static_cast<CControlUI*>(m_pm.FindControl(pt));
+		if (pControl && _tcscmp(pControl->GetClass(), L"ButtonUI") != 0) return HTCAPTION;
+	}
+	return HTCLIENT;
+}
+
+LRESULT CCaigouDetailWnd::OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+{
+	SIZE szRoundCorner = m_pm.GetRoundCorner();
+	if (!::IsIconic(*this) && (szRoundCorner.cx != 0 || szRoundCorner.cy != 0))
+	{
+		CDuiRect rcWnd; ::GetWindowRect(*this, &rcWnd);
+		rcWnd.Offset(-rcWnd.left, -rcWnd.top); rcWnd.right++; rcWnd.bottom++;
+		HRGN hRgn = ::CreateRoundRectRgn(rcWnd.left, rcWnd.top, rcWnd.right, rcWnd.bottom, szRoundCorner.cx, szRoundCorner.cy);
+		::SetWindowRgn(*this, hRgn, TRUE); ::DeleteObject(hRgn);
+	}
+	bHandled = FALSE;
+	return 0;
+}
+
+LRESULT CCaigouDetailWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+	LRESULT lRes = 0; BOOL bHandled = TRUE;
+	switch (uMsg)
+	{
+	case WM_CREATE: lRes = OnCreate(uMsg, wParam, lParam, bHandled); break;
+	case WM_NCACTIVATE: lRes = OnNcActivate(uMsg, wParam, lParam, bHandled); break;
+	case WM_NCCALCSIZE: lRes = OnNcCalcSize(uMsg, wParam, lParam, bHandled); break;
+	case WM_NCPAINT: lRes = OnNcPaint(uMsg, wParam, lParam, bHandled); break;
+	case WM_NCHITTEST: lRes = OnNcHitTest(uMsg, wParam, lParam, bHandled); break;
+	case WM_SIZE: lRes = OnSize(uMsg, wParam, lParam, bHandled); break;
+	case WM_KEYDOWN:
+		if (wParam == VK_ESCAPE) { Close(IDCANCEL); return TRUE; }
+		bHandled = FALSE; break;
+	default: bHandled = FALSE;
+	}
+	if (bHandled) return lRes;
+	if (m_pm.MessageHandler(uMsg, wParam, lParam, lRes)) return lRes;
+	return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
+}
+
+LRESULT CCaigouDetailWnd::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+{
+	LONG styleValue = ::GetWindowLong(*this, GWL_STYLE);
+	styleValue &= ~WS_CAPTION;
+	::SetWindowLong(*this, GWL_STYLE, styleValue | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
+
+	m_pm.Init(m_hWnd);
+	m_pm.AddPreMessageFilter(this);
+
+	CDialogBuilder builder;
+	CControlUI* pRoot = builder.Create(_T("cg_doc_detail_wnd.xml"), (UINT)0, NULL, &m_pm);
+	ASSERT(pRoot && "CCaigouDetailWnd: 皮肤 XML 解析失败");
+	m_pm.AttachDialog(pRoot);
+	m_pm.AddNotifier(this);
+	return 0;
+}
+
+void CCaigouDetailWnd::Notify(TNotifyUI& msg)
+{
+	if (msg.sType != _T("click")) return;
+	std::wstring n = (LPCTSTR)msg.pSender->GetName();
+	if (n == L"kd_close" || n == L"kd_btn_close") { Close(IDCANCEL); return; }
+	if (n == L"kd_btn_print") { m_bPrint = true; Close(IDOK); return; }
+}
+
+void CCaigouDetailWnd::LoadDetail()
+{
+	FillInfo();
+	FillRows();
+	FillSum();
+
+	int n = (int)m_doc.rows.size();
+	int rows = (n > 0) ? n : 1;
+	CControlUI* pCard = m_pm.FindControl(L"kd_tblcard");
+	if (pCard != NULL) pCard->SetFixedHeight(45 + 52 * rows + 10);
+
+	::SetWindowPos(m_hWnd, NULL, 0, 0, 660, WindowHeightFor(n),
+		SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
+}
+
+void CCaigouDetailWnd::FillInfo()
+{
+	CgDSet(m_pm, L"kd_title", L"采购订单详情");
+	CgDSet(m_pm, L"kd_sub", L"采购单详情");
+
+	CgDSet(m_pm, L"kd_l1", L"采购单号");
+	CgDSet(m_pm, L"kd_v1", m_doc.no);
+	CgDSet(m_pm, L"kd_l2", L"状态");
+	CgDValue(m_pm, L"kd_v2", CgStatusText(m_doc.status), CgStatusStyle(m_doc.status));
+	CgDSet(m_pm, L"kd_l3", L"经手人");
+	CgDSet(m_pm, L"kd_v3", m_doc.handler);
+	CgDSet(m_pm, L"kd_l4", L"下单时间");
+	CgDSet(m_pm, L"kd_v4", m_doc.time);
+	CgDSet(m_pm, L"kd_l5", L"商品数");
+	CgDSet(m_pm, L"kd_v5", std::to_wstring(m_doc.goods) + L" 项");
+	CgDSet(m_pm, L"kd_l6", L"备注");
+	CgDSet(m_pm, L"kd_v6", m_doc.note.empty() ? L"—" : m_doc.note);
+}
+
+void CCaigouDetailWnd::FillRows()
+{
+	CListUI* pList = static_cast<CListUI*>(m_pm.FindControl(L"kd_list"));
+	if (pList == NULL) return;
+	while (pList->GetCount() > 0) pList->RemoveAt(0);
+
+	for (size_t i = 0; i < m_doc.rows.size(); i++)
+	{
+		const CgDetailRow& r = m_doc.rows[i];
+		CListContainerElementUI* row = CgDNewRow(&m_pm, L"cg_row_doc_detail.xml");
+		if (row == NULL) continue;
+		CLabelUI* pName = static_cast<CLabelUI*>(row->FindSubControl(L"col_name"));
+		CLabelUI* pUnit = static_cast<CLabelUI*>(row->FindSubControl(L"col_unit"));
+		CLabelUI* pQty = static_cast<CLabelUI*>(row->FindSubControl(L"col_qty"));
+		CLabelUI* pPrice = static_cast<CLabelUI*>(row->FindSubControl(L"col_price"));
+		CLabelUI* pAmt = static_cast<CLabelUI*>(row->FindSubControl(L"col_amount"));
+		if (pName) pName->SetText(r.name.c_str());
+		if (pUnit) pUnit->SetText(r.unit.c_str());
+		if (pQty) pQty->SetText(KcFormatQty(r.qty).c_str());
+		if (pPrice) pPrice->SetText(KcFormatMoney(r.price).c_str());
+		if (pAmt) pAmt->SetText(KcFormatMoney(r.amount).c_str());
+		pList->Add(row);
+	}
+}
+
+void CCaigouDetailWnd::FillSum()
+{
+	int n = (int)m_doc.rows.size();
+	double total = 0.0;
+	for (size_t i = 0; i < m_doc.rows.size(); i++) total += m_doc.rows[i].amount;
+	CgDSet(m_pm, L"kd_sum_lab", L"商品 " + std::to_wstring(n) + L" 种 · 合计");
+	CgDSet(m_pm, L"kd_sum_amt", L"¥" + KcFormatMoney(total));
+}

+ 338 - 0
zhipuzi_pos_windows/page/CCaigouViews.h

@@ -0,0 +1,338 @@
+#pragma once
+
+#include "pch.h"
+#include <string>
+#include <vector>
+#include "../ui/CBasePageUI.h"
+// 复用库存页已验证的格式化 / 标签 / 商品档案 / 原生 Edit 同步助手
+#include "CKucunViews.h"
+#include "../ui/CModalWnd.h"
+
+// ============================================================================
+// 采购页各视图(记录 / 统计 / 新建采购单)+ 采购订单详情弹框 + 添加商品弹层
+//
+// 架构与库存页(CKucunViews)同构,但采购页没有一级 Tab:主态 = 单棵 cg_scroll
+// 里「周期行 + 概览带 + 二级 ovseg(记录/统计) + 筛选行 + 记录卡 / 统计卡」。
+// 新建采购单是挂在 cg_page 下的整页 cg_view_order_new,用 SetVisible 与主态互斥。
+// 页面级浮层:cg_keypad(数字键盘,新建页用)+ cg_pick_overlay(添加商品弹层)。
+//
+// 全部用静态样例数据(与设计稿口径一致),后端接口接入时替换 CgAllRec / CgAllStat /
+// CgLookupDetail 即可,视图与弹框逻辑不动。
+// ============================================================================
+
+// 分类下拉项(与商品档案 cat 取值一一对应;CG_CATS 在 CCaigouViews.cpp 定义,本文件仅声明)
+extern const wchar_t* const CG_CATS[6];
+
+// ---------------- 采购状态 ----------------
+// 待审核(橙) / 已通过(绿) / 已拒绝(红) / 已完成(绿)
+enum
+{
+	CG_ST_PENDING = 0,
+	CG_ST_AUDITED = 1,
+	CG_ST_REJECTED = 2,
+	CG_ST_DONE = 3,
+};
+
+// 状态文案
+inline LPCWSTR CgStatusText(int st)
+{
+	switch (st)
+	{
+	case CG_ST_PENDING:  return L"待审核";
+	case CG_ST_AUDITED:  return L"已通过";
+	case CG_ST_REJECTED: return L"已拒绝";
+	case CG_ST_DONE:     return L"已完成";
+	default:             return L"未知";
+	}
+}
+
+// 状态 → 胶囊标签样式下标(复用库存页 kc_tag_* 资源)
+inline int CgStatusStyle(int st)
+{
+	switch (st)
+	{
+	case CG_ST_PENDING:  return KC_TAG_ORG;   // 橙
+	case CG_ST_AUDITED:  return KC_TAG_GRN;   // 绿
+	case CG_ST_REJECTED: return KC_TAG_RED;   // 红
+	case CG_ST_DONE:     return KC_TAG_GRN;   // 绿
+	default:             return KC_TAG_GRAY;
+	}
+}
+
+// 周期分段(与库存页同语义:0 本月 / 1 上月 / 2 近三个月,按自然月)
+void CgPeriodBounds(int period, std::wstring& from, std::wstring& to);
+bool CgInPeriod(const std::wstring& date, int period);
+std::wstring CgPeriodRangeText(int period);
+
+// ---------------- 采购记录(记录卡) ----------------
+struct CgRecItem
+{
+	std::wstring no;       // 采购单号
+	std::wstring date;     // 下单时间 YYYY-MM-DD HH:MM
+	std::wstring handler;  // 经手人
+	int          goods;    // 商品数
+	double       amount;   // 金额(元)
+	int          status;   // CG_ST_*
+};
+
+// ---------------- 采购统计(按商品维度) ----------------
+struct CgStatItem
+{
+	std::wstring name, cat, unit;
+	int          orderCnt; // 采购单数
+	int          qty;      // 采购数量
+	double       amount;   // 金额(元)
+	// ratio(占比) 由 BuildStatRows 统计总金额后现算
+};
+
+// ---------------- 新建采购单 · 明细行 ----------------
+struct CgEditLine
+{
+	std::wstring name, cat, unit;   // 商品名称 / 分类 / 单位
+	std::wstring qty;               // 采购数量(可编辑,键盘清空补 0)
+	std::wstring price;             // 单价(元,可编辑)
+	// amount = qty * price,由 RefreshRow 现算
+};
+
+// ---------------- 采购订单详情(弹框) ----------------
+struct CgDetailRow
+{
+	std::wstring name, unit;
+	double       qty, price, amount;
+};
+struct CgDocDetail
+{
+	std::wstring no;        // 单号
+	int          status;    // CG_ST_*
+	std::wstring handler;   // 经手人
+	std::wstring time;      // 下单时间
+	int          goods;     // 商品数
+	std::wstring note;      // 备注
+	std::vector<CgDetailRow> rows;
+};
+// 按单号取整单详情;未命中返回 false
+bool CgLookupDetail(const std::wstring& no, CgDocDetail& out);
+
+// 添加商品弹层候选项(复用库存页 KcPickItem:name/cat/unit/stock/on)
+typedef KcPickItem CgPickItem;
+
+// 视图基类:持有宿主容器(cg_scroll / cg_view_order_new)+ 管理器,提供查控件 / 建行 / 填统计卡
+class CCaigouViewBase
+{
+public:
+	CCaigouViewBase(CPaintManagerUI* pm, CContainerUI* host)
+		: m_pManager(pm), m_pHost(host) {}
+	virtual ~CCaigouViewBase() {}
+
+	virtual void InitShow() = 0;
+	virtual void OnInitControl() {}
+	virtual void OnClick(TNotifyUI& msg) {}
+	virtual void OnItemSelect(TNotifyUI& msg) {}
+	virtual void OnTextChanged(TNotifyUI& msg) {}
+	virtual void SubmitSearch() {}
+	virtual int GetPeriod() const { return m_nPeriod; }
+	virtual std::wstring PrefixW() const { return L""; }
+
+protected:
+	virtual CControlUI* Find(LPCTSTR name);
+	template<typename T> T* FindC(LPCTSTR name) { return static_cast<T*>(Find(name)); }
+
+	CListContainerElementUI* NewRow(LPCTSTR xmlfile);
+	void ClearList(CListUI* list);
+	void SetRowText(CListContainerElementUI* row, LPCTSTR name, LPCTSTR text);
+	void SetRowColor(CListContainerElementUI* row, LPCTSTR name, DWORD color);
+	void SetRowWidth(CListContainerElementUI* row, LPCTSTR name, int width);
+	void SetTag(CListContainerElementUI* row, LPCTSTR name, LPCTSTR text, int style);
+
+	// 统计卡(26px 粗体数字 + 13px 灰单位,showhtml)
+	void SetStatInt(LPCTSTR name, int val, LPCWSTR unit);
+	void SetStatMoney(LPCTSTR name, double val, LPCWSTR unit);
+	void SetListCount(LPCTSTR labelName, int n);
+
+	// 按名字给 autocalcwidth 标签写文本 —— 内部转 CCaigouViewBase::SetLabelText,
+	// 口径与原因见下方 SetLabelText 的注释(务必读一遍再写标签)。
+	void SetLabel(LPCTSTR name, LPCTSTR text, DWORD color = 0);
+
+	// 整页滚动高度传播:List 撑成「表头44 + nRows*行高」并沿父链写死
+	void FitListHeight(LPCTSTR listName, int nRows, int rowH);
+
+	void AddComboItem(CComboUI* combo, LPCWSTR text);
+	void SelectCombo(CComboUI* combo, int idx);
+	void ReadSearch(LPCTSTR editName, std::wstring& out);
+	void SyncSearchTishi(LPCTSTR tishiName, LPCTSTR editName);
+
+public:
+	// 二级 ovseg 三态(采购记录 / 采购统计,复用 kc_seggrn_* / kc_seggray_*)
+	static void StyleSeg(CButtonUI* p, bool act);
+	// 周期分段三态(复用 kc_ovseg_*);宿主页(CCaigouPageUI)也要刷周期段,故放 public
+	static void StylePeriodSeg(CButtonUI* p, bool act);
+
+	// ⭐【R75】给 autocalcwidth 标签写文本的**唯一正确姿势**(逐字照搬库存页
+	// CKucunEditViewBase::SetLabel / R48)。视图与宿主页共用,避免各写一份又漏步骤。
+	//
+	// 为什么不能只 SetText 就完事 —— 已在 DuiLib 源码里坐实(UILabel.cpp):
+	//   · m_bNeedEstimateSize 构造时 = false(UILabel.cpp:15);
+	//   · CLabelUI::SetText(:328)在 autocalcwidth 下确实会 NeedParentUpdate(),
+	//     但**不置** m_bNeedEstimateSize;
+	//   · CLabelUI::EstimateSize(:114)只有当 m_bNeedEstimateSize 为真时才**按当前文字
+	//     重新量宽**,否则直接返回上一次缓存的 m_cxyFixed;
+	//   · CLabelUI::SetFont(:74)是唯一**无条件**置 m_bNeedEstimateSize = true 的路子。
+	//   ⇒ 只调 SetText:父布局重排了,但标签报出的还是**旧文字**的宽度,多出来的字符被裁掉。
+	//     症状:键盘面板输入 555 只显示 5;统计卡 0 → 12,345 被截断;周期区间日期尾巴没了。
+	//   ⚠️ 凡是为 autocalcwidth 标签动态写文本都必须走这里,不要直接 SetText。
+	//      调用方负责传对控件(CControlUI* 会被当成 CLabelUI 用,别拿 Button 进来)。
+	static void SetLabelText(CControlUI* p, LPCTSTR text, DWORD color = 0);
+
+protected:
+	CPaintManagerUI* m_pManager;
+	CContainerUI* m_pHost;
+	int m_nPeriod = 0;
+};
+
+// ---------------- 主态视图(记录 + 统计 双卡) ----------------
+class CCaigouMainView : public CCaigouViewBase
+{
+public:
+	CCaigouMainView(CPaintManagerUI* pm, CContainerUI* host) : CCaigouViewBase(pm, host) {}
+	virtual void OnInitControl();
+	virtual void InitShow();
+	virtual void OnClick(TNotifyUI& msg);
+	virtual void OnItemSelect(TNotifyUI& msg);
+	virtual void OnTextChanged(TNotifyUI& msg);
+	virtual void SubmitSearch();
+
+	// 由宿主页在「新建采购单」按钮点击时调用,切到新建态之前不必做啥;
+	// 这里保留接口,便于将来主态联动。
+	void SetSeg(int active, bool bBuild = true);   // 0 = 记录 1 = 统计
+	void SetPeriod(int period, bool bBuild = true);
+
+private:
+	void RefreshOverview();                        // 概览带 4 格按当前周期重算
+	void BuildRecordRows();
+	void BuildStatRows();
+	void ResetFilter();
+	void ApplyRowFilter();                         // 按状态 / 搜索过滤记录行(记录在内存,真实接口前先用全量)
+
+	int m_nSeg = 0;
+	std::wstring m_strSearch;
+	int m_nStatus = 0;                            // 0 = 全部状态
+	// 统计卡「分类」下拉的当前选中(0 = 全部分类;k>0 ⇒ CG_CATS[k-1])。
+	// ⚠️ 该筛选**只作用于统计卡**(分类是商品维度属性,记录卡按单号维度、没有分类列)。
+	int m_nStatCat = 0;
+	std::vector<CgRecItem> m_rec;                 // 当前周期内的记录(内存副本,用于过滤)
+};
+
+// ---------------- 新建采购单子视图(独立整页) ----------------
+class CCaigouNewView : public CCaigouViewBase
+{
+public:
+	CCaigouNewView(CPaintManagerUI* pm, CContainerUI* host, CContainerUI* kpHost)
+		: CCaigouViewBase(pm, host), m_pKpHost(kpHost), m_nEditRow(-1), m_nEditCol(0) {}
+	virtual void OnInitControl();
+	virtual void InitShow();
+	virtual void OnClick(TNotifyUI& msg);
+
+	// 宿主页在「添加商品」弹层确定时调用:把勾选商品追加进明细(已在明细里的去重)
+	void AppendPicked(const std::vector<std::wstring>& names);
+	void CloseKeypad();
+
+	// 该单元格编辑态(键盘面板用)
+	bool IsEditing() const { return m_nEditRow >= 0; }
+
+	// 【保存校验】采集「采购数量为空 / ≤ 0」的行(口径参照入库 CollectQtyIssues)
+	void CollectQtyIssues(std::vector<KcQtyIssue>& out) const;
+	// 重刷所有明细单元格边框色(编辑中绿 / 数量为 0 琥珀 / 常态白)
+	void ApplyAllCellStyles();
+	// 宿主页在「数量校验弹框」里点了「删除该行」时回调(与库存页 DeleteLine 同法)
+	void DeleteLine(int idx);
+
+protected:
+	// 覆写 Find:键盘面板 cg_keypad 与底栏 cg_new_foot 是**兄弟节点**(同挂页面级容器),
+	// 既不在本视图子树(cg_view_order_new)里、也不在键盘面板里。所以找 cg_kp_* / cg_cgn_foot_*
+	// 这类控件时,必须依次在本视图 → 键盘面板 → 键盘面板的父级(共同父级)里查。
+	// 与库存页 CKucunEditViewBase::Find 完全同法(R49)。
+	virtual CControlUI* Find(LPCTSTR name);
+
+	LPCTSTR RowXml() const { return L"cg_row_edit.xml"; }
+	LPCTSTR Prefix() const { return L"cg_cgn"; }
+	virtual std::wstring PrefixW() const { return std::wstring(Prefix()); }
+	LPCTSTR ListName() const { return L"cg_cgn_list"; }
+	LPCTSTR TblCardName() const { return L"cg_cgn_tblcard"; }
+
+	void SeedLines();                              // 塞入初始占位明细
+	void BuildRows();
+	void RefreshAll();
+	void SyncTableHeight();
+	void RefreshRow(int i);
+	void RefreshTot();
+
+	// 数量校验 / 单元格着色(参照入库同套口径)
+	bool IsQtyZero(int i) const;                   // 数量是否为 0(空串不算)
+	void ApplyCellStyle(int i, int col);           // 单元格底图/文字色(编辑绿 / 0 琥珀 / 常态白)
+	void NormalizeBuf();                           // 键盘缓冲清空即归 0
+
+	// 键盘
+	void OpenKeypad(int row, int col);
+	void KeypadRefresh();
+	void KpDigit(LPCTSTR t);
+	void KpDot();
+	void KpDel();
+	void KpClear();
+	void KpApply();
+	void KpCommit();
+
+	std::vector<CgEditLine> m_lines;
+	CContainerUI* m_pKpHost;
+	int m_nEditRow;
+	int m_nEditCol;
+	std::wstring m_editBuf;
+};
+
+// ---------------- 采购订单详情弹框 ----------------
+// 复用 CModalWnd(真模态,与 CKucunDetailWnd 同骨架)。皮肤 = cg_doc_detail_wnd.xml(5 列)。
+class CCaigouDetailWnd : public CModalWnd
+{
+public:
+	explicit CCaigouDetailWnd(const CgDocDetail& doc) : m_doc(doc) {}
+	virtual ~CCaigouDetailWnd() {}
+
+	LPCTSTR GetWindowClassName() const { return _T("CgDocDetailWndFrame"); }
+	UINT GetClassStyle() const { return UI_CLASSSTYLE_DIALOG; }
+	void OnFinalMessage(HWND /*hWnd*/) { m_pm.RemovePreMessageFilter(this); }
+	void Notify(TNotifyUI& msg);
+	LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
+	LRESULT OnNcActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+	{
+		if (::IsIconic(*this)) bHandled = FALSE;
+		return (wParam == 0) ? TRUE : FALSE;
+	}
+	LRESULT OnNcCalcSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 0; }
+	LRESULT OnNcPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 0; }
+	LRESULT OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
+	LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
+	LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
+	void HandleTextCapture(std::string /*content*/) {}
+	LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, bool& bHandled) { return false; }
+
+	// 铺数据 + 按明细行数调窗高。Create 之后、CenterWindow 之前调用。
+	void LoadDetail();
+	bool IsPrintClicked() const { return m_bPrint; }
+
+	// 窗高 = 366 + 52*nRows(头 56 + 主体 502 + 脚 68;5 行=626)
+	static int WindowHeightFor(int nRows)
+	{
+		int n = (nRows > 0) ? nRows : 1;
+		return 366 + 52 * n;
+	}
+
+public:
+	CPaintManagerUI m_pm;
+
+private:
+	void FillInfo();
+	void FillRows();
+	void FillSum();
+
+	CgDocDetail m_doc;
+	bool m_bPrint = false;
+};

BIN
zhipuzi_pos_windows/resource/skin.zip


+ 2 - 0
zhipuzi_pos_windows/zhipuzi_pos_windows.vcxproj

@@ -294,6 +294,7 @@ copy $(ProjectDir)conf\ $(SolutionDir)bin\$(Platform)\$(Configuration)\conf\</Co
     <ClInclude Include="tool\CFoodHandle.h" />
     <ClInclude Include="page\CSaomashouyinPageUI.h" />
     <ClInclude Include="page\CCaigouPageUI.h" />
+    <ClInclude Include="page\CCaigouViews.h" />
     <ClInclude Include="page\CKucunPageUI.h" />
     <ClInclude Include="page\CKucunViews.h" />
     <ClInclude Include="wnd\CKucunDetailWnd.h" />
@@ -441,6 +442,7 @@ copy $(ProjectDir)conf\ $(SolutionDir)bin\$(Platform)\$(Configuration)\conf\</Co
     <ClCompile Include="tool\CFoodHandle.cpp" />
     <ClCompile Include="page\CSaomashouyinPageUI.cpp" />
     <ClCompile Include="page\CCaigouPageUI.cpp" />
+    <ClCompile Include="page\CCaigouViews.cpp" />
     <ClCompile Include="page\CKucunPageUI.cpp" />
     <ClCompile Include="page\CKucunViews.cpp" />
     <ClCompile Include="wnd\CKucunDetailWnd.cpp" />