feat(miniapp): 新增小羚羚小程序主体与审批中心等页面,并重构年审管理为支持嵌入XLL主题

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-09 18:07:37 +08:00
parent 9e66af3eb8
commit 351688006e
5 changed files with 8622 additions and 85 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2152,6 +2152,43 @@ const PAGE_STYLE = `
}
`;
const XLL_GREEN = '#7AB929';
const XLL_GREEN_DEEP = '#6AA322';
const XLL_GREEN_SOFT = 'rgba(122, 185, 41, 0.14)';
const EMBED_STYLE = `
.ar-embed-root {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: ${COLOR_PAGE};
position: relative;
}
.ar-embed-root .ar-tabs,
.ar-embed-root .ar-search-row { flex-shrink: 0; }
.ar-embed-root .ar-list,
.ar-embed-root .ar-operate-scroll,
.ar-embed-root .ar-history-scroll { flex: 1; min-height: 0; }
`;
const XLL_THEME_PATCH = `
.xll-module-theme .ar-tab.active { color: ${XLL_GREEN}; }
.xll-module-theme .ar-tab.active::after { background: ${XLL_GREEN}; }
.xll-module-theme .ar-filter-btn:active { border-color: ${XLL_GREEN}; color: ${XLL_GREEN}; }
.xll-module-theme .ar-card-action { color: ${XLL_GREEN}; }
.xll-module-theme .ar-add-btn { color: ${XLL_GREEN_DEEP}; }
.xll-module-theme .ar-mp-link { color: ${XLL_GREEN_DEEP}; }
.xll-module-theme .ar-form-control .ant-select-focused .ant-select-selector {
background: ${XLL_GREEN_SOFT} !important;
}
.xll-module-theme .ar-operate-foot .ant-btn-primary {
background: linear-gradient(135deg, ${XLL_GREEN} 0%, ${XLL_GREEN_DEEP} 100%) !important;
border: none !important;
}
`;
const IconFilter = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={COLOR_PRIMARY_DEEP} strokeWidth="2" strokeLinecap="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
@@ -3310,7 +3347,7 @@ const AnnualReviewPrdDoc = () => (
</div>
);
const Component = function () {
const AnnualReviewPanel = function AnnualReviewPanel({ embedded = false, theme = 'default', onBack, onOpenPrd }) {
const [mainTab, setMainTab] = useState('pending');
const [searchPlate, setSearchPlate] = useState('');
const [plateKbOpen, setPlateKbOpen] = useState(false);
@@ -3336,6 +3373,8 @@ const Component = function () {
const [licenseForm, setLicenseForm] = useState({ ...EMPTY_LICENSE_FORM });
const [operateDrafts, setOperateDrafts] = useState(() => loadOperateDrafts());
const licenseOcrTimerRef = useRef(null);
const themeClass = theme === 'xll' ? ' xll-module-theme' : '';
const embedStyles = `${PAGE_STYLE}${EMBED_STYLE}${theme === 'xll' ? XLL_THEME_PATCH : ''}`;
const resetOperateForms = () => {
if (licenseOcrTimerRef.current) {
@@ -3431,6 +3470,22 @@ const Component = function () {
setHistoryViewTask(null);
};
useEffect(() => {
if (!embedded || typeof window === 'undefined') return undefined;
window.__xllInspectionBack = () => {
if (historyViewTask) {
closeHistoryViewPage();
return true;
}
if (operateTask) {
closeOperatePage();
return true;
}
return false;
};
return () => { delete window.__xllInspectionBack; };
}, [embedded, historyViewTask, operateTask]);
const setInspection = (key, val) => setInspectionForm((p) => ({ ...p, [key]: val }));
const setM2 = (key, val) => setM2Form((p) => ({ ...p, [key]: val }));
const setZb = (key, val) => setZbForm((p) => ({ ...p, [key]: val }));
@@ -4078,92 +4133,85 @@ const Component = function () {
);
};
return (
<div className="ar-mini-root">
<style>{PAGE_STYLE}</style>
<div className="ar-phone">
<MiniProgramChrome
title={historyViewTask ? '年审记录' : operateTask ? '年审操作' : '年审'}
showBack={!!operateTask || !!historyViewTask}
onBack={historyViewTask ? closeHistoryViewPage : closeOperatePage}
showPrdLink={!operateTask && !historyViewTask}
onPrdClick={() => setPrdOpen(true)}
/>
const phoneMain = (
<>
{historyViewTask ? (
renderHistoryDetailPage()
) : operateTask ? (
renderOperatePage()
) : (
<>
<div className="ar-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={mainTab === 'pending'}
className={`ar-tab${mainTab === 'pending' ? ' active' : ''}`}
onClick={() => setMainTab('pending')}
>
待处理
</button>
<button
type="button"
role="tab"
aria-selected={mainTab === 'history'}
className={`ar-tab${mainTab === 'history' ? ' active' : ''}`}
onClick={() => setMainTab('history')}
>
历史记录
</button>
</div>
{historyViewTask ? (
renderHistoryDetailPage()
) : operateTask ? (
renderOperatePage()
) : (
<>
<div className="ar-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={mainTab === 'pending'}
className={`ar-tab${mainTab === 'pending' ? ' active' : ''}`}
onClick={() => setMainTab('pending')}
>
待处理
</button>
<button
type="button"
role="tab"
aria-selected={mainTab === 'history'}
className={`ar-tab${mainTab === 'history' ? ' active' : ''}`}
onClick={() => setMainTab('history')}
>
历史记录
</button>
<div className="ar-search-row">
<div
className="ar-search-box"
role="button"
tabIndex={0}
onClick={openPlateKeyboard}
onKeyDown={(e) => e.key === 'Enter' && openPlateKeyboard()}
>
{!searchPlate ? (
<span className="ar-search-placeholder" aria-hidden="true">
请输入车牌号
</span>
) : null}
<input
readOnly
value={searchPlate}
aria-label="车牌号搜索"
onFocus={(e) => {
e.target.blur();
openPlateKeyboard();
}}
/>
</div>
<button type="button" className="ar-filter-btn" aria-label="打开筛选" onClick={openFilter}>
<IconFilter />
</button>
</div>
<div className="ar-search-row">
<div
className="ar-search-box"
role="button"
tabIndex={0}
onClick={openPlateKeyboard}
onKeyDown={(e) => e.key === 'Enter' && openPlateKeyboard()}
>
{!searchPlate ? (
<span className="ar-search-placeholder" aria-hidden="true">
请输入车牌号
</span>
) : null}
<input
readOnly
value={searchPlate}
aria-label="车牌号搜索"
onFocus={(e) => {
e.target.blur();
openPlateKeyboard();
}}
/>
</div>
<button type="button" className="ar-filter-btn" aria-label="打开筛选" onClick={openFilter}>
<IconFilter />
</button>
</div>
<div className="ar-list">
{filteredTasks.length === 0 ? (
<div className="ar-empty">暂无符合条件的年审任务<br />请调整搜索或筛选条件</div>
) : (
filteredTasks.map(renderTaskCard)
)}
</div>
</>
)}
<div className="ar-list">
{filteredTasks.length === 0 ? (
<div className="ar-empty">暂无符合条件的年审任务<br />请调整搜索或筛选条件</div>
) : (
filteredTasks.map(renderTaskCard)
)}
</div>
</>
)}
<PlateKeyboardPanel
open={plateKbOpen && !operateTask && !historyViewTask}
value={plateKbDraft}
onChange={setPlateKbDraft}
onConfirm={confirmPlateKeyboard}
onClose={closePlateKeyboard}
/>
</div>
<PlateKeyboardPanel
open={plateKbOpen && !operateTask && !historyViewTask}
value={plateKbDraft}
onChange={setPlateKbDraft}
onConfirm={confirmPlateKeyboard}
onClose={closePlateKeyboard}
/>
</>
);
const moduleOverlays = (
<>
<Drawer
title="筛选"
placement="right"
@@ -4184,7 +4232,9 @@ const Component = function () {
style={{
height: 46,
borderRadius: 8,
background: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
background: theme === 'xll'
? `linear-gradient(135deg, ${XLL_GREEN} 0%, ${XLL_GREEN_DEEP} 100%)`
: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
border: 'none'
}}
>
@@ -4236,7 +4286,7 @@ const Component = function () {
</Drawer>
<Modal
open={prdOpen}
open={embedded ? false : prdOpen}
title="年审管理 · 产品需求说明"
onCancel={() => setPrdOpen(false)}
footer={[
@@ -4244,7 +4294,10 @@ const Component = function () {
key="ok"
type="primary"
onClick={() => setPrdOpen(false)}
style={{ background: COLOR_PRIMARY_DEEP, borderColor: COLOR_PRIMARY_DEEP }}
style={{
background: theme === 'xll' ? XLL_GREEN_DEEP : COLOR_PRIMARY_DEEP,
borderColor: theme === 'xll' ? XLL_GREEN_DEEP : COLOR_PRIMARY_DEEP,
}}
>
知道了
</Button>
@@ -4255,8 +4308,45 @@ const Component = function () {
>
<AnnualReviewPrdDoc />
</Modal>
</>
);
if (embedded) {
return (
<div className={`ar-embed-root${themeClass}`}>
<style>{embedStyles}</style>
{phoneMain}
{moduleOverlays}
</div>
);
}
return (
<div className="ar-mini-root">
<style>{PAGE_STYLE}</style>
<div className="ar-phone">
<MiniProgramChrome
title={historyViewTask ? '年审记录' : operateTask ? '年审操作' : '年审'}
showBack={!!operateTask || !!historyViewTask}
onBack={historyViewTask ? closeHistoryViewPage : closeOperatePage}
showPrdLink={!operateTask && !historyViewTask}
onPrdClick={() => (onOpenPrd ? onOpenPrd() : setPrdOpen(true))}
/>
<div className="ar-embed-root">{phoneMain}</div>
</div>
{moduleOverlays}
</div>
);
};
const Component = function AnnualReviewMiniApp() {
return <AnnualReviewPanel embedded={false} />;
};
if (typeof window !== 'undefined') {
window.Component = Component;
window.ONEOS_MP_EMBED = window.ONEOS_MP_EMBED || {};
window.ONEOS_MP_EMBED.AnnualReviewPanel = AnnualReviewPanel;
}
export default Component;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
// 【重要】必须使用 const Component 作为组件变量名
// ONE-OS 小程序 - 还车应结款审批办理(参照 web 端还车应结款-查看,适配移动端)
// 独立预览页:完整审批能力已内嵌于「审批中心.jsx」本页供 Axhub 单独打开预览。
const { useState, useMemo, useRef, useEffect } = React;
const moment = window.moment || window.dayjs;
const COLOR_PRIMARY = '#16D1A1';
const COLOR_PRIMARY_DEEP = '#00BFA5';
const COLOR_PRIMARY_SOFT = 'rgba(22, 209, 161, 0.12)';
const COLOR_TEXT = '#1D2129';
const COLOR_TEXT_SEC = '#4E5969';
const COLOR_MUTED = '#86909C';
const COLOR_LINE = '#E5E6EB';
const COLOR_BG = '#FFFFFF';
const COLOR_PAGE = '#F2F3F5';
const COLOR_SUCCESS = '#00B42A';
const COLOR_DANGER = '#F53F3F';
const COLOR_WARN = '#FF7D00';
const FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, sans-serif';
const antd = (() => {
const raw = window.antd;
if (!raw) return {};
return raw.default && typeof raw.default === 'object' ? { ...raw, ...raw.default } : raw;
})();
const message = antd.message || { info: () => {}, success: () => {}, warning: () => {} };
const Drawer = antd.Drawer;
const formatMoney = (val) => {
const n = parseFloat(val);
if (Number.isNaN(n)) return val || '—';
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};
const formatYuan = (val) => {
const n = parseFloat(val);
if (Number.isNaN(n)) return '—';
return `${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元`;
};
const DEFAULT_TASK = {
bizNo: 'HC-2026-0418',
plateNo: '粤B58888F',
customerName: '嘉兴某某物流有限公司',
projectName: '嘉兴腾4.5T租赁',
pendingSettle: '927.50',
depositAmount: '5000.00',
refundTotal: '4072.50',
payTotal: '0.00',
actualRent: '0.00',
};
const Component = function ReturnSettlementApproveStandalone() {
const task = window.__returnSettlementTask || DEFAULT_TASK;
const handleBack = () => {
if (window.__returnSettlementBack) window.__returnSettlementBack();
else message.info('返回审批中心(原型)');
};
return (
<div className="hc-mini-root">
<style>{`
.hc-mini-root { min-height:100dvh; background:linear-gradient(165deg,#e8ebef 0%,${COLOR_PAGE} 40%); display:flex; justify-content:center; padding:16px 12px 32px; font-family:${FONT_FAMILY}; }
.hc-phone { width:100%; max-width:390px; min-height:844px; background:${COLOR_PAGE}; border-radius:28px; overflow:hidden; box-shadow:0 24px 48px rgba(15,23,42,.14); display:flex; flex-direction:column; position:relative; }
.hc-chrome { background:${COLOR_BG}; border-bottom:1px solid rgba(0,0,0,.05); padding:44px 8px 0; }
.hc-nav { height:48px; display:flex; align-items:center; position:relative; }
.hc-nav-title { flex:1; text-align:center; font-size:17px; font-weight:700; }
.hc-back { width:40px; height:40px; border:none; background:transparent; cursor:pointer; }
.hc-tip { padding:24px; text-align:center; color:${COLOR_MUTED}; font-size:14px; line-height:1.6; }
`}</style>
<div className="hc-phone">
<div className="hc-chrome">
<div className="hc-nav">
<button type="button" className="hc-back" onClick={handleBack} aria-label="返回"></button>
<div className="hc-nav-title">还车应结款审批</div>
<span style={{ width: 40 }} />
</div>
</div>
<div className="hc-tip">
完整还车应结款审批页含结算明细费用分组审批操作已内嵌于
<strong> 审批中心.jsx </strong>
请从我的待办 还车应结款 HC-2026-0418 去审批进入体验
<br /><br />
待结算总额<strong style={{ color: '#8B5CF6' }}>{formatMoney(task.pendingSettle)}</strong>
<br />
{task.plateNo} · {task.customerName}
</div>
</div>
</div>
);
};
if (typeof window !== 'undefined') window.Component = Component;
export default Component;