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

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;