| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>申勤物业 · 数字化平台</title>
- <style>
- :root {
- --bg: #f4f6f8;
- --surface: #fff;
- --border: #e8ecef;
- --text: #1a2b3c;
- --text2: #5c6f82;
- --text3: #8a9bab;
- --primary: #0d9488;
- --primary-soft: #e6f6f4;
- --warn: #d97706;
- --danger: #dc2626;
- --radius: 10px;
- --font: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
- }
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body { font-family: var(--font); font-size: 14px; color: var(--text); background: var(--bg); line-height: 1.5; }
- a { color: var(--primary); text-decoration: none; cursor: pointer; }
- a:hover { text-decoration: underline; }
- .hidden { display: none !important; }
- /* layout */
- .layout { display: flex; min-height: 100vh; }
- .sidebar {
- width: 220px; flex-shrink: 0; background: var(--surface);
- border-right: 1px solid var(--border); padding: 16px 12px;
- display: flex; flex-direction: column;
- }
- .brand { display: flex; align-items: center; gap: 10px; padding: 8px 10px 20px; }
- .brand-icon {
- width: 36px; height: 36px; background: var(--primary); border-radius: 8px;
- color: #fff; display: flex; align-items: center; justify-content: center;
- font-weight: 700; font-size: 14px;
- }
- .brand strong { display: block; font-size: 14px; }
- .brand span { font-size: 11px; color: var(--text3); }
- .nav a {
- display: block; padding: 8px 12px; border-radius: 6px; color: var(--text2);
- font-size: 13px; margin-bottom: 2px;
- }
- .nav a:hover { background: #f8fafb; color: var(--text); text-decoration: none; }
- .nav a.active { background: var(--primary-soft); color: var(--primary); font-weight: 500; }
- .nav-label { font-size: 11px; color: var(--text3); padding: 12px 12px 6px; font-weight: 600; }
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
- .topbar {
- min-height: 52px; background: var(--surface); border-bottom: 1px solid var(--border);
- display: flex; align-items: center; justify-content: space-between;
- padding: 10px 24px; gap: 16px; flex-wrap: wrap;
- }
- .topbar h1 { font-size: 17px; font-weight: 600; }
- .topbar p { font-size: 12px; color: var(--text3); }
- .content { padding: 20px 24px 32px; flex: 1; }
- /* components */
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }
- .card-head { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 14px; }
- .card-body { padding: 16px; }
- .stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-bottom: 20px; }
- .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
- .stat label { font-size: 12px; color: var(--text3); display: block; margin-bottom: 4px; }
- .stat b { font-size: 22px; font-weight: 600; }
- .stat small { font-size: 11px; color: var(--text2); display: block; margin-top: 4px; }
- .stat.warn b { color: var(--danger); }
- .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); }
- th { color: var(--text3); font-size: 12px; font-weight: 600; }
- .tag { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: #ecfdf5; color: #059669; }
- .tag-warn { background: #fff7ed; color: var(--warn); }
- .tag-danger { background: #fef2f2; color: var(--danger); }
- .btn {
- display: inline-block; padding: 7px 14px; border-radius: 6px; font-size: 13px;
- font-weight: 500; border: 1px solid var(--border); background: #fff;
- color: var(--text2); cursor: pointer; font-family: inherit;
- }
- .btn:hover { background: #f8fafb; }
- .btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
- .btn-primary:hover { background: #0b7d73; }
- .btn-danger { background: var(--danger); color: #fff; border-color: var(--danger); }
- .btn-ghost { border-color: transparent; background: transparent; }
- .proto-note {
- background: #fffbeb; border: 1px solid #fde68a; color: #92400e;
- padding: 10px 14px; border-radius: 8px; font-size: 12px; margin-bottom: 16px;
- }
- .banner-renewal {
- background: linear-gradient(90deg, #fff7ed, #fff);
- border: 1px solid #fed7aa; border-left: 4px solid var(--warn);
- padding: 12px 16px; border-radius: var(--radius); margin-bottom: 16px;
- display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
- }
- .banner-renewal.urgent {
- background: linear-gradient(90deg, #fef2f2, #fff);
- border-color: #fecaca; border-left-color: var(--danger);
- }
- .banner-renewal strong { color: var(--danger); }
- .todo-list { list-style: none; }
- .todo-item { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); }
- .todo-item:last-child { border-bottom: none; }
- .todo-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); margin-top: 6px; flex-shrink: 0; }
- .todo-dot.urgent { background: var(--danger); animation: pulse 1.5s infinite; }
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
- .quick-links { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
- .quick-card {
- display: block; padding: 14px; border: 1px solid var(--border); border-radius: 8px;
- background: #fafbfc; text-decoration: none; color: inherit;
- }
- .quick-card:hover { border-color: var(--primary); background: var(--primary-soft); text-decoration: none; }
- .quick-card strong { display: block; font-size: 14px; margin-bottom: 4px; }
- .quick-card span { font-size: 12px; color: var(--text3); }
- .quick-card--primary { border-color: var(--primary); background: var(--primary-soft); }
- .stage-bar {
- display: flex; gap: 6px; align-items: center; padding: 12px 16px;
- background: #fff; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 16px;
- }
- .dot { width: 10px; height: 10px; border-radius: 50%; background: #e8ecef; }
- .dot.done { background: #0d9488; }
- .dot.cur { background: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); }
- .info { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; font-size: 13px; }
- .info dt { color: var(--text3); }
- .info-wide { grid-template-columns: minmax(140px, 168px) 1fr; }
- .card-note { font-size: 11px; color: var(--text3); font-weight: 400; margin-left: 8px; }
- .checklist { list-style: none; }
- .checklist li { padding: 8px 0; border-bottom: 1px dashed var(--border); font-size: 13px; display: flex; gap: 8px; }
- .checklist li.done { color: var(--text3); text-decoration: line-through; }
- /* GIS full page */
- .view-gis { position: fixed; inset: 0; z-index: 100; background: #e8ecef; }
- .gis-top {
- position: absolute; top: 0; left: 0; right: 0; z-index: 10; height: 48px;
- background: rgba(255, 255, 255, 0.95); border-bottom: 1px solid var(--border);
- display: flex; align-items: center; gap: 12px; padding: 0 16px 0 280px;
- }
- .gis-top h1 { font-size: 14px; font-weight: 600; }
- .gis-back { font-size: 12px; color: var(--text2); }
- .gis-left {
- position: absolute; top: 56px; left: 12px; bottom: 40px; width: 268px; z-index: 10;
- background: rgba(255, 255, 255, 0.96); border: 1px solid var(--border); border-radius: 10px;
- display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
- }
- .gis-left .search { padding: 10px; border-bottom: 1px solid var(--border); }
- .gis-left .search input { width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; }
- .gis-left .count { padding: 6px 12px; font-size: 11px; color: var(--text3); }
- .gis-list { flex: 1; overflow-y: auto; padding: 0 8px 10px; font-size: 12px; }
- .gis-item {
- display: block; width: 100%; text-align: left; padding: 8px 10px; border: none;
- border-radius: 6px; background: transparent; cursor: pointer; font-family: inherit;
- margin-bottom: 2px;
- }
- .gis-item:hover, .gis-item.on { background: var(--primary-soft); }
- .gis-item b { display: block; font-size: 13px; color: var(--text); }
- .gis-item span { color: var(--text3); font-size: 11px; }
- .gis-item .renew-badge { font-size: 10px; color: var(--danger); font-weight: 600; }
- .map-mock {
- position: absolute; inset: 0;
- background: linear-gradient(180deg, #f0f4f8 0%, #e2e8ec 100%);
- background-image:
- linear-gradient(rgba(255, 255, 255, 0.6) 1px, transparent 1px),
- linear-gradient(90deg, rgba(255, 255, 255, 0.6) 1px, transparent 1px);
- background-size: 48px 48px;
- }
- .map-road {
- position: absolute; background: #fff; border: 1px solid #d5dde3; border-radius: 4px;
- }
- .map-road.r1 { left: 15%; top: 35%; width: 55%; height: 12px; transform: rotate(-8deg); }
- .map-road.r2 { left: 40%; top: 20%; width: 8px; height: 50%; }
- .pin-project {
- position: absolute; width: 16px; height: 16px; background: #0d9488;
- border: 2px solid #fff; border-radius: 50%; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
- z-index: 2; cursor: pointer; transform: translate(-50%, -50%);
- }
- .pin-project.renew { background: var(--danger); animation: pin-pulse 2s infinite; }
- @keyframes pin-pulse {
- 0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.5); }
- 70% { box-shadow: 0 0 0 12px rgba(220, 38, 38, 0); }
- 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
- }
- .zone { position: absolute; border-radius: 8px; pointer-events: none; }
- .zone.renew { background: rgba(220, 38, 38, 0.15); border: 2px dashed var(--danger); }
- .gis-tools {
- position: absolute; top: 64px; right: 12px; z-index: 10;
- background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 8px;
- font-size: 11px; color: var(--text2); min-width: 120px;
- }
- .gis-tools label { display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer; }
- .gis-info-card {
- position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 10;
- width: min(420px, 90vw); background: #fff; border: 1px solid var(--border);
- border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); padding: 16px;
- }
- .gis-info-card header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
- .gis-info-card h3 { font-size: 15px; }
- .gis-info-card dl { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; font-size: 12px; margin-bottom: 12px; }
- .gis-info-card dt { color: var(--text3); }
- .gis-info-card .actions { display: flex; gap: 8px; flex-wrap: wrap; }
- /* bid workbench */
- .steps { display: flex; gap: 4px; margin-bottom: 20px; flex-wrap: wrap; }
- .step {
- padding: 6px 14px; border-radius: 20px; font-size: 12px;
- background: #f1f5f9; color: var(--text3);
- }
- .step.active { background: var(--primary-soft); color: var(--primary); font-weight: 600; }
- .step.done { background: #ecfdf5; color: #059669; }
- .bid-task-row {
- display: flex; align-items: center; justify-content: space-between;
- padding: 14px 16px; border-bottom: 1px solid var(--border); gap: 12px;
- }
- .bid-task-row:last-child { border-bottom: none; }
- .bid-workspace {
- display: grid;
- grid-template-columns: minmax(200px, 0.85fr) minmax(240px, 1fr) minmax(200px, 0.85fr);
- grid-template-rows: minmax(240px, 36vh) minmax(38vh, 1fr);
- gap: 12px;
- flex: 1;
- min-height: calc(100vh - 240px);
- }
- .bid-col {
- background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
- display: flex; flex-direction: column; overflow: hidden; min-height: 0;
- }
- .bid-col-head {
- padding: 10px 14px; border-bottom: 1px solid var(--border);
- font-weight: 600; font-size: 13px; background: #fafbfc; flex-shrink: 0;
- }
- .bid-col-body { padding: 12px 14px; flex: 1; overflow-y: auto; font-size: 13px; min-height: 0; }
- .bid-blocks-compact { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
- .block-item {
- border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; cursor: pointer;
- transition: border-color 0.12s, background 0.12s;
- }
- .block-item.compact { padding: 8px 10px; margin-bottom: 0; }
- .block-item.compact strong { font-size: 13px; }
- .block-item.active { border-color: var(--primary); background: var(--primary-soft); }
- .block-meta { font-size: 11px; color: var(--text3); margin-top: 4px; }
- .bid-editor-zone {
- grid-column: 1 / -1;
- background: var(--surface);
- border: 2px solid var(--primary);
- border-radius: var(--radius);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-height: 38vh;
- }
- .bid-editor-zone.readonly { border-color: var(--border); }
- .bid-editor-zone:focus-within { box-shadow: 0 0 0 4px var(--primary-soft); }
- .bid-editor-toolbar {
- display: flex; justify-content: space-between; align-items: center; gap: 12px;
- padding: 12px 16px; background: linear-gradient(180deg, #fafbfc, #fff);
- border-bottom: 1px solid var(--border); flex-shrink: 0; flex-wrap: wrap;
- }
- .bid-editor-toolbar-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
- .bid-editor-toolbar-left h3 { font-size: 15px; font-weight: 600; margin: 0; }
- .bid-editor-toolbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
- .bid-editor-body { flex: 1; display: flex; flex-direction: column; min-height: 0; background: #fff; }
- .bid-draft-editor {
- flex: 1;
- width: 100%;
- min-height: calc(38vh - 108px);
- padding: 20px 24px;
- border: none;
- border-radius: 0;
- font-family: inherit;
- font-size: 14px;
- line-height: 1.85;
- color: var(--text);
- resize: none;
- outline: none;
- background: #fff;
- tab-size: 4;
- }
- .bid-draft-editor::placeholder { color: var(--text3); }
- .bid-draft-readonly {
- flex: 1;
- min-height: calc(38vh - 108px);
- padding: 20px 24px;
- font-size: 14px;
- line-height: 1.85;
- color: var(--text2);
- white-space: pre-wrap;
- overflow-y: auto;
- background: #fafbfc;
- }
- .bid-editor-foot {
- padding: 8px 16px;
- border-top: 1px solid var(--border);
- font-size: 11px;
- color: var(--text3);
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 12px;
- flex-shrink: 0;
- background: #fafbfc;
- }
- .bid-ref-scroll { max-height: 100%; overflow-y: auto; }
- .mapping-table.compact { font-size: 11px; }
- .mapping-table.compact td, .mapping-table.compact th { padding: 5px 6px; }
- #view-bid-task .content.bid-task-content {
- display: flex;
- flex-direction: column;
- min-height: 0;
- padding-bottom: 16px;
- }
- .bid-three-col {
- display: grid; grid-template-columns: 1fr 1.2fr 1fr; gap: 12px; min-height: 480px;
- }
- .fill-auto { color: #059669; }
- .fill-confirm { color: var(--warn); }
- .fill-manual { color: #dc2626; }
- .doc-line { padding: 6px 8px; border-radius: 4px; margin-bottom: 4px; cursor: pointer; font-size: 12px; }
- .doc-line:hover { background: #f1f5f9; }
- .doc-line.highlight { background: #fff7ed; border-left: 3px solid var(--warn); }
- .preview-doc { font-size: 12px; line-height: 1.7; color: var(--text2); }
- .preview-doc h4 { color: var(--text); font-size: 14px; margin: 12px 0 6px; }
- .preview-doc h4:first-child { margin-top: 0; }
- .empty-shell {
- text-align: center; padding: 48px 24px; color: var(--text3);
- border: 2px dashed var(--border); border-radius: var(--radius);
- }
- .empty-shell p { margin-top: 8px; font-size: 13px; }
- /* tender pool */
- .market-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 0; }
- .market-tab {
- padding: 10px 18px; border: none; background: none; font-family: inherit;
- font-size: 13px; color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px;
- }
- .market-tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
- .pool-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
- .pool-toolbar input, .pool-toolbar select {
- padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px;
- }
- .pool-chips { display: flex; gap: 6px; flex-wrap: wrap; }
- .chip {
- padding: 5px 12px; border-radius: 16px; font-size: 12px; border: 1px solid var(--border);
- background: #fff; cursor: pointer; font-family: inherit; color: var(--text2);
- }
- .chip.active { background: var(--primary-soft); border-color: var(--primary); color: var(--primary); font-weight: 500; }
- .pool-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px; }
- .pool-stat {
- background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
- padding: 12px 14px; cursor: pointer; transition: border-color 0.15s;
- }
- .pool-stat:hover { border-color: var(--primary); }
- .pool-stat b { display: block; font-size: 20px; margin-top: 2px; }
- .pool-stat span { font-size: 11px; color: var(--text3); }
- .pool-stat.highlight { border-color: var(--danger); background: #fef2f2; }
- .tender-row {
- display: grid; grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px;
- border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.12s;
- }
- .tender-row:hover { background: #f8fafb; }
- .tender-row.selected { background: var(--primary-soft); }
- .tender-row:last-child { border-bottom: none; }
- .tender-title { font-weight: 600; font-size: 14px; margin-bottom: 6px; }
- .tender-meta { font-size: 12px; color: var(--text3); display: flex; flex-wrap: wrap; gap: 12px; }
- .tender-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
- .score-badge {
- font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
- background: #ecfdf5; color: #059669;
- }
- .score-badge.high { background: #fef2f2; color: var(--danger); }
- .tag-renewal { background: #fef2f2; color: var(--danger); }
- .tag-new { background: #eff6ff; color: #2563eb; }
- /* drawer & toast */
- .drawer-mask {
- position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 200;
- display: flex; justify-content: flex-end;
- }
- .drawer {
- width: min(480px, 95vw); background: #fff; height: 100%; overflow-y: auto;
- box-shadow: -4px 0 24px rgba(0,0,0,0.12); animation: slideIn 0.2s ease;
- }
- @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
- .drawer-head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
- .drawer-body { padding: 16px 20px; }
- .drawer-section { margin-bottom: 20px; }
- .drawer-section h4 { font-size: 13px; color: var(--text3); margin-bottom: 8px; font-weight: 600; }
- .source-link-box {
- display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
- background: #f8fafb; border: 1px solid var(--border); border-radius: 8px; margin-top: 8px;
- }
- .source-link-box a { word-break: break-all; font-size: 12px; line-height: 1.5; }
- .toast-wrap { position: fixed; bottom: 24px; right: 24px; z-index: 300; display: flex; flex-direction: column; gap: 8px; }
- .toast {
- background: #1a2b3c; color: #fff; padding: 10px 16px; border-radius: 8px;
- font-size: 13px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); animation: fadeUp 0.25s ease;
- max-width: 360px;
- }
- @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
- .progress-bar { height: 6px; background: #e8ecef; border-radius: 3px; overflow: hidden; margin: 12px 0; }
- .progress-bar i { display: block; height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.3s; }
- .checklist li.clickable { cursor: pointer; }
- .checklist li.clickable:hover { background: #f8fafb; margin: 0 -8px; padding-left: 8px; padding-right: 8px; border-radius: 4px; }
- .stat.clickable { cursor: pointer; }
- .stat.clickable:hover { border-color: var(--primary); }
- .block-item.confirmed { border-color: #059669; background: #ecfdf5; }
- .block-item.confirmed .block-meta::after { content: ' · 已确认'; color: #059669; font-weight: 600; }
- .doc-line.active { background: var(--primary-soft); border-left: 3px solid var(--primary); }
- .flow-mini { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text3); margin-top: 12px; flex-wrap: wrap; }
- .flow-mini span.done { color: #059669; }
- .flow-mini span.cur { color: var(--primary); font-weight: 600; }
- /* bid result & push */
- .form-grid { display: grid; grid-template-columns: 120px 1fr; gap: 10px 14px; align-items: center; font-size: 13px; max-width: 640px; }
- .form-grid label { color: var(--text2); }
- .form-grid input, .form-grid select, .form-grid textarea {
- padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; font-family: inherit; font-size: 13px; width: 100%;
- }
- .form-grid textarea { min-height: 60px; resize: vertical; }
- .monitor-card {
- border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 12px;
- background: #fff; transition: border-color 0.15s;
- }
- .monitor-card.active { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }
- .monitor-card.won { border-color: #059669; background: #f0fdf4; }
- .monitor-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
- .monitor-meta { font-size: 12px; color: var(--text3); display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 10px; }
- .monitor-status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
- .monitor-status .pulse-dot {
- width: 8px; height: 8px; border-radius: 50%; background: var(--primary); animation: pulse 1.5s infinite;
- }
- .push-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 14px; }
- .push-item {
- border: 1px solid var(--border); border-radius: 8px; padding: 12px; text-align: center; font-size: 12px;
- background: #fafbfc;
- }
- .push-item.done { border-color: #059669; background: #ecfdf5; }
- .push-item strong { display: block; font-size: 13px; margin-bottom: 4px; }
- .timeline { list-style: none; margin-top: 12px; }
- .timeline li { padding: 8px 0 8px 16px; border-left: 2px solid var(--border); margin-left: 6px; font-size: 12px; color: var(--text2); position: relative; }
- .timeline li::before {
- content: ''; position: absolute; left: -5px; top: 12px; width: 8px; height: 8px;
- border-radius: 50%; background: var(--primary);
- }
- .timeline li.done::before { background: #059669; }
- .result-banner {
- background: linear-gradient(90deg, #ecfdf5, #fff); border: 1px solid #a7f3d0; border-left: 4px solid #059669;
- padding: 14px 16px; border-radius: var(--radius); margin-bottom: 16px;
- }
- .result-banner.lost { background: linear-gradient(90deg, #fef2f2, #fff); border-color: #fecaca; border-left-color: var(--danger); }
- .page-tabs { display: flex; gap: 0; margin-bottom: 16px; border-bottom: 1px solid var(--border); }
- .page-tab {
- padding: 10px 18px; border: none; background: none; font-family: inherit; font-size: 13px;
- color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px;
- }
- .page-tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
- /* renewal hub */
- .renewal-hero {
- background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
- color: #fff; border-radius: var(--radius); padding: 20px 24px; margin-bottom: 16px;
- display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; flex-wrap: wrap;
- }
- .renewal-hero h2 { font-size: 20px; margin-bottom: 4px; }
- .renewal-hero p { font-size: 13px; opacity: 0.9; }
- .renewal-countdown {
- text-align: center; background: rgba(255,255,255,0.15); border-radius: 10px;
- padding: 12px 20px; min-width: 100px;
- }
- .renewal-countdown b { display: block; font-size: 32px; line-height: 1; }
- .renewal-countdown span { font-size: 11px; opacity: 0.85; }
- .renewal-stepper {
- display: flex; align-items: center; gap: 0; margin-bottom: 20px; overflow-x: auto;
- padding: 12px 16px; background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
- }
- .renewal-step {
- display: flex; align-items: center; flex-shrink: 0; font-size: 12px; color: var(--text3);
- }
- .renewal-step-dot {
- width: 28px; height: 28px; border-radius: 50%; background: #e8ecef; color: var(--text3);
- display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 11px;
- margin-right: 6px; flex-shrink: 0;
- }
- .renewal-step.done .renewal-step-dot { background: #059669; color: #fff; }
- .renewal-step.active .renewal-step-dot { background: var(--primary); color: #fff; box-shadow: 0 0 0 3px var(--primary-soft); }
- .renewal-step.active { color: var(--primary); font-weight: 600; }
- .renewal-step-arrow { color: var(--border); margin: 0 8px; font-size: 14px; }
- .renewal-layout { display: grid; grid-template-columns: 1fr 340px; gap: 16px; align-items: start; }
- .action-card {
- border: 2px solid var(--primary); border-radius: var(--radius); background: var(--primary-soft);
- padding: 18px 20px; margin-bottom: 16px;
- }
- .action-card h3 { font-size: 16px; margin-bottom: 8px; color: var(--text); }
- .action-card p { font-size: 13px; color: var(--text2); margin-bottom: 14px; }
- .action-card .actions { display: flex; gap: 8px; flex-wrap: wrap; }
- .side-card { background: #fff; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 12px; }
- .side-card-head { padding: 10px 14px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 13px; }
- .side-card-body { padding: 12px 14px; font-size: 13px; }
- .tender-mini {
- border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-top: 8px;
- background: #fafbfc;
- }
- .tender-mini.matched { border-color: var(--danger); background: #fef2f2; }
- .tender-mini.monitoring { border-color: var(--warn); background: #fffbeb; }
- /* renewal list */
- .renewal-row {
- display: grid; grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px;
- border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.12s;
- }
- .renewal-row:hover { background: #f8fafb; }
- .renewal-row:last-child { border-bottom: none; }
- .renewal-row-title { font-weight: 600; font-size: 14px; margin-bottom: 6px; }
- .renewal-row-meta { font-size: 12px; color: var(--text3); display: flex; flex-wrap: wrap; gap: 12px; }
- .renewal-row-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
- .days-badge {
- font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
- background: #fef2f2; color: var(--danger);
- }
- .days-badge.warn { background: #fff7ed; color: var(--warn); }
- .days-badge.ok { background: #ecfdf5; color: #059669; }
- /* pipeline lists (投标/储备) */
- .pipeline-row {
- display: grid; grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px;
- border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.12s;
- }
- .pipeline-row:hover { background: #f8fafb; }
- .pipeline-row:last-child { border-bottom: none; }
- .pipeline-row-title { font-weight: 600; font-size: 14px; margin-bottom: 6px; }
- .pipeline-row-meta { font-size: 12px; color: var(--text3); display: flex; flex-wrap: wrap; gap: 12px; }
- .pipeline-row-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
- .tag-bid { background: #eff6ff; color: #2563eb; }
- .tag-reserve { background: #f5f3ff; color: #7c3aed; }
- /* bid task deepen */
- .bid-summary-bar {
- display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px;
- }
- .bid-summary-item {
- background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
- padding: 12px 14px; font-size: 12px;
- }
- .bid-summary-item b { display: block; font-size: 18px; margin-top: 2px; color: var(--text); }
- .bid-summary-item span { color: var(--text3); }
- .bid-detail-panel {
- background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
- margin-top: 12px; overflow: hidden;
- }
- .bid-detail-head {
- padding: 10px 14px; border-bottom: 1px solid var(--border);
- font-weight: 600; font-size: 13px; background: #fafbfc;
- display: flex; justify-content: space-between; align-items: center; gap: 8px;
- }
- .bid-detail-body { padding: 14px; }
- .mapping-table { width: 100%; font-size: 12px; margin-top: 8px; }
- .mapping-table th { font-size: 11px; padding: 6px 8px; }
- .mapping-table td { padding: 8px; vertical-align: top; }
- .mapping-ok { color: #059669; }
- .mapping-warn { color: var(--warn); }
- .req-list { list-style: none; margin: 8px 0; }
- .req-list li {
- padding: 6px 0 6px 12px; border-left: 2px solid var(--border); margin-bottom: 6px;
- font-size: 12px; color: var(--text2);
- }
- .req-list li.matched { border-left-color: var(--primary); }
- .history-compare {
- background: #fffbeb; border: 1px solid #fde68a; border-radius: 6px;
- padding: 10px 12px; font-size: 12px; color: #92400e; margin-top: 10px;
- }
- .score-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
- .score-item {
- text-align: center; padding: 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px;
- }
- .score-item b { display: block; font-size: 16px; color: var(--primary); margin-bottom: 2px; }
- @media (max-width: 1100px) {
- .renewal-layout { grid-template-columns: 1fr; }
- .stats { grid-template-columns: repeat(3, 1fr); }
- .pool-stats { grid-template-columns: repeat(2, 1fr); }
- .push-grid { grid-template-columns: repeat(2, 1fr); }
- .bid-workspace {
- grid-template-columns: 1fr;
- grid-template-rows: auto auto auto minmax(42vh, 1fr);
- min-height: auto;
- }
- .bid-editor-zone { grid-column: 1; min-height: 42vh; }
- .bid-draft-editor, .bid-draft-readonly { min-height: calc(42vh - 108px); }
- .bid-summary-bar { grid-template-columns: repeat(2, 1fr); }
- .gis-top { padding-left: 16px; }
- }
- </style>
- </head>
- <body>
- <div class="layout" id="app-shell">
- <aside class="sidebar">
- <div class="brand">
- <div class="brand-icon">申</div>
- <div><strong>申勤物业</strong><span>数字化平台</span></div>
- </div>
- <nav class="nav" id="main-nav">
- <a href="#dashboard" data-route="dashboard">工作台</a>
- <a href="#gis" data-route="gis">空间一张图</a>
- <a href="#projects" data-route="projects">项目台账</a>
- <div class="nav-label">市场拓展</div>
- <a href="#market" data-route="market">市场拓展中心</a>
- <a href="#renewal" data-route="renewal">项目续标</a>
- <a href="#tender-pool" data-route="tender-pool">招标监测</a>
- <a href="#bid-workbench" data-route="bid-workbench">标书编制</a>
- <a href="#bid-result" data-route="bid-result">开标与中标</a>
- </nav>
- </aside>
- <div class="main">
- <!-- ========== 工作台 ========== -->
- <section id="view-dashboard" class="view">
- <header class="topbar">
- <div><h1>工作台</h1><p>今日待办与关键指标 · 续标预警优先展示</p></div>
- <input type="search" placeholder="搜索项目、人员…" style="padding:6px 12px;border:1px solid var(--border);border-radius:6px;width:200px" />
- </header>
- <div class="content">
- <div class="stats" id="dashboard-stats"></div>
- <div class="grid2">
- <div class="card">
- <div class="card-head">今日待办 <span class="tag tag-danger" style="float:right">续标优先</span></div>
- <div class="card-body"><ul class="todo-list" id="todo-list"></ul></div>
- </div>
- <div class="card">
- <div class="card-head">快捷入口</div>
- <div class="card-body">
- <div class="quick-links">
- <a class="quick-card" href="#market">
- <strong>市场拓展中心</strong><span>招标 · 标书 · 投标 · 续标</span>
- </a>
- <a class="quick-card" href="#renewal">
- <strong>项目续标</strong><span>续标项目列表 · 全流程跟进</span>
- </a>
- <a class="quick-card quick-card--primary" href="#gis">
- <strong>空间一张图</strong><span>查看续标预警项目</span>
- </a>
- <a class="quick-card" href="#bid-workbench">
- <strong>标书编制</strong><span>拆解 · 确认 · 生成</span>
- </a>
- </div>
- </div>
- </div>
- </div>
- <div class="card">
- <div class="card-head">近期项目</div>
- <div class="card-body" style="padding:0">
- <table>
- <thead><tr><th>项目简称</th><th>在管服务</th><th>合同起止</th><th>线上</th><th>状态</th></tr></thead>
- <tbody id="recent-projects"></tbody>
- </table>
- </div>
- </div>
- </div>
- </section>
- <!-- ========== 项目台账 ========== -->
- <section id="view-projects" class="view hidden">
- <header class="topbar">
- <div><h1>项目台账</h1><p>在管项目一览</p></div>
- </header>
- <div class="content">
- <div class="card">
- <div class="card-body" style="padding:0">
- <table>
- <thead><tr><th>项目简称</th><th>金蝶编码</th><th>合同起止</th><th>距到期</th><th>操作</th></tr></thead>
- <tbody id="project-list"></tbody>
- </table>
- </div>
- </div>
- </div>
- </section>
- <!-- ========== 项目详情 ========== -->
- <section id="view-project" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#projects">← 项目台账</a></p>
- <h1 id="project-title">—</h1>
- <p id="project-sub">—</p>
- </div>
- <div style="display:flex;gap:8px;flex-wrap:wrap">
- <button type="button" class="btn" onclick="navigate('gis')">地图定位</button>
- <button type="button" class="btn btn-primary" id="btn-start-renewal">发起续标</button>
- </div>
- </header>
- <div class="content">
- <div id="project-banner"></div>
- <div class="stage-bar">
- <span class="dot done"></span><span class="dot done"></span><span class="dot done"></span>
- <span class="dot cur"></span><span class="dot"></span><span class="dot"></span>
- <span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
- <span style="margin-left:8px;font-size:13px;color:var(--text2)">当前:合同执行 · 即将进入续约/投标</span>
- </div>
- <div class="grid2">
- <div class="card">
- <div class="card-head">项目与甲方</div>
- <div class="card-body"><dl class="info info-wide" id="project-base"></dl></div>
- </div>
- <div class="card">
- <div class="card-head">合同与服务</div>
- <div class="card-body"><dl class="info info-wide" id="project-contract"></dl></div>
- </div>
- <div class="card">
- <div class="card-head">合同金额</div>
- <div class="card-body"><dl class="info info-wide" id="project-finance"></dl></div>
- </div>
- <div class="card">
- <div class="card-head">历史投标</div>
- <div class="card-body"><dl class="info" id="project-bids"></dl></div>
- </div>
- </div>
- <div class="card">
- <div class="card-head">续标入口</div>
- <div class="card-body">
- <p style="font-size:13px;color:var(--text2);margin-bottom:12px">续标请从项目续标列表进入,或直接进入本项目的续标详情页。</p>
- <button type="button" class="btn" onclick="navigate('renewal')">续标列表</button>
- <button type="button" class="btn btn-primary" style="margin-left:8px" onclick="startRenewal()">进入本项目续标 →</button>
- </div>
- </div>
- </div>
- </section>
- <!-- ========== 项目续标列表 ========== -->
- <section id="view-renewal" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#market">← 市场拓展中心</a></p>
- <h1>项目续标</h1>
- <p>合同到期预警 · 续标全流程跟进</p>
- </div>
- </header>
- <div class="content">
- <div class="pool-stats" id="renewal-stats"></div>
- <div class="card">
- <div class="card-head">续标项目列表</div>
- <div class="card-body" style="padding:12px 16px 0">
- <div class="pool-toolbar">
- <input type="search" id="renewal-search" placeholder="搜索项目简称、甲方…" style="flex:1;min-width:180px" oninput="renderRenewalList()" />
- <select id="renewal-region" onchange="renderRenewalList()">
- <option value="">全部区域</option>
- <option value="西">西</option>
- <option value="东">东</option>
- </select>
- </div>
- <div class="pool-chips" id="renewal-chips"></div>
- </div>
- <div id="renewal-list" style="padding:0"></div>
- </div>
- </div>
- </section>
- <!-- ========== 项目续标详情 ========== -->
- <section id="view-renewal-detail" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#renewal">← 项目续标</a> · <a href="#project">项目详情</a> · <a href="#market">市场拓展中心</a></p>
- <h1 id="renewal-page-title">项目续标</h1>
- <p id="renewal-page-sub">续标全流程 · 从预警到中标推送</p>
- </div>
- <div style="display:flex;gap:8px;flex-wrap:wrap" id="renewal-header-actions"></div>
- </header>
- <div class="content" id="renewal-content"></div>
- </section>
- <!-- ========== 市场拓展 ========== -->
- <section id="view-market" class="view hidden">
- <header class="topbar">
- <div><h1>市场拓展中心</h1><p>招标监测 · 标书编制 · 投标履约 · 项目续标</p></div>
- <button type="button" class="btn btn-primary" onclick="navigate('renewal')">进入项目续标 →</button>
- </header>
- <div class="content">
- <div class="grid2">
- <div class="card" style="border-color:var(--primary)">
- <div class="card-head">项目续标 <span class="tag tag-warn" style="float:right">进行中</span></div>
- <div class="card-body">
- <p>合同预警 · 招标跟踪 · 标书编制 · 开标中标</p>
- <p style="margin-top:12px"><button type="button" class="btn btn-primary" onclick="navigate('renewal')">进入项目续标 →</button></p>
- </div>
- </div>
- <div class="card" style="border-color:var(--primary);cursor:pointer" onclick="navigate('tender-pool')">
- <div class="card-head">招标监测 <span class="tag tag-danger" id="market-pool-badge" style="float:right">今日 +3</span></div>
- <div class="card-body">
- <p>政府采购网抓取 · 关键词订阅 · 转投标 / 转储备</p>
- <p style="margin-top:8px;font-size:12px;color:var(--text3)" id="tender-monitor-status">—</p>
- <p style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
- <span class="btn btn-primary">查看招标信息 →</span>
- </p>
- <p style="margin-top:8px;font-size:12px"><a href="#tender-bids" onclick="event.stopPropagation()">投标项目</a> · <a href="#tender-reserve" onclick="event.stopPropagation()">储备项目</a></p>
- </div>
- </div>
- <div class="card" style="border-color:var(--primary)">
- <div class="card-head">标书编制 <span class="tag tag-warn" style="float:right" id="market-bid-badge">进行中 1</span></div>
- <div class="card-body">
- <p>需求拆解 · 数据填充 · 分步确认 · 生成导出</p>
- <p style="margin-top:12px"><button type="button" class="btn btn-primary" onclick="navigate('bid-workbench')">进入标书编制 →</button></p>
- </div>
- </div>
- <div class="card" style="cursor:pointer" onclick="navigate('bid-result')">
- <div class="card-head">开标与中标 <span class="tag tag-warn" id="market-result-badge" style="float:right">监控 0</span></div>
- <div class="card-body">
- <p>投标登记 · 开标监控 · 结果抓取 · 中标推送</p>
- <p style="margin-top:10px"><span class="btn btn-primary">进入 →</span></p>
- </div>
- </div>
- </div>
- </div>
- </section>
- <!-- ========== 招标监测 ========== -->
- <section id="view-tender-pool" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#market">← 市场拓展中心</a></p>
- <h1>招标监测</h1>
- <p>政府采购网抓取 · 续标监控 · 智能匹配</p>
- </div>
- <div style="display:flex;gap:8px;flex-wrap:wrap">
- <button type="button" class="btn" onclick="navigate('tender-bids')">投标项目 <span id="pool-link-bids"></span></button>
- <button type="button" class="btn" onclick="navigate('tender-reserve')">储备项目 <span id="pool-link-reserve"></span></button>
- <button type="button" class="btn" onclick="simulateTenderMatch()">模拟:续标公告匹配</button>
- <button type="button" class="btn btn-primary" onclick="navigate('bid-workbench')">标书编制</button>
- </div>
- </header>
- <div class="content">
- <div class="pool-stats" id="pool-stats"></div>
- <div class="card">
- <div class="card-head">信息列表</div>
- <div class="card-body" style="padding:12px 16px 0">
- <div class="pool-toolbar">
- <input type="search" id="pool-search" placeholder="搜索项目名称、关键词…" style="flex:1;min-width:180px" oninput="renderTenderPool()" />
- <select id="pool-source" onchange="renderTenderPool()">
- <option value="">全部来源</option>
- <option value="上海政府采购网">上海政府采购网</option>
- <option value="中国政府采购网">中国政府采购网</option>
- </select>
- <select id="pool-district" onchange="renderTenderPool()">
- <option value="">全部区域</option>
- <option value="青浦区">青浦区</option>
- <option value="徐汇区">徐汇区</option>
- </select>
- </div>
- <div class="pool-chips" id="pool-chips"></div>
- </div>
- <div id="tender-list" style="padding:0"></div>
- </div>
- </div>
- </section>
- <!-- ========== 投标项目列表 ========== -->
- <section id="view-tender-bids" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#tender-pool">← 招标监测</a> · <a href="#market">市场拓展中心</a></p>
- <h1>投标项目</h1>
- <p>从招标监测转入 · 标书编制 · 投标跟进</p>
- </div>
- <button type="button" class="btn btn-primary" onclick="navigate('bid-workbench')">标书编制 →</button>
- </header>
- <div class="content">
- <div class="pool-stats" id="bid-pipeline-stats"></div>
- <div class="card">
- <div class="card-head">投标项目列表</div>
- <div class="card-body" style="padding:12px 16px 0">
- <div class="pool-toolbar">
- <input type="search" id="bid-pipeline-search" placeholder="搜索项目名称…" style="flex:1;min-width:180px" oninput="renderTenderBidList()" />
- </div>
- <div class="pool-chips" id="bid-pipeline-chips"></div>
- </div>
- <div id="bid-pipeline-list" style="padding:0"></div>
- </div>
- </div>
- </section>
- <!-- ========== 储备项目列表 ========== -->
- <section id="view-tender-reserve" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#tender-pool">← 招标监测</a> · <a href="#market">市场拓展中心</a></p>
- <h1>储备项目</h1>
- <p>从招标监测转入 · 业态评估 · 择机转投标</p>
- </div>
- </header>
- <div class="content">
- <div class="pool-stats" id="reserve-stats"></div>
- <div class="card">
- <div class="card-head">储备项目列表</div>
- <div class="card-body" style="padding:12px 16px 0">
- <div class="pool-toolbar">
- <input type="search" id="reserve-search" placeholder="搜索项目名称、区域…" style="flex:1;min-width:180px" oninput="renderTenderReserveList()" />
- <select id="reserve-district" onchange="renderTenderReserveList()">
- <option value="">全部区域</option>
- <option value="青浦区">青浦区</option>
- <option value="徐汇区">徐汇区</option>
- </select>
- </div>
- <div class="pool-chips" id="reserve-chips"></div>
- </div>
- <div id="reserve-list" style="padding:0"></div>
- </div>
- </div>
- </section>
- <!-- ========== 标书编制列表 ========== -->
- <section id="view-bid-workbench" class="view hidden">
- <header class="topbar">
- <div><h1>标书编制</h1><p>需求拆解 · 数据映射 · 分步确认 · 生成导出</p></div>
- <button type="button" class="btn btn-primary" onclick="navigate('bid-task')">继续编制 →</button>
- </header>
- <div class="content">
- <div class="steps">
- <span class="step done">1 上传招标文件</span>
- <span class="step active">2 智能拆解</span>
- <span class="step">3 分步确认</span>
- <span class="step">4 生成导出</span>
- </div>
- <div class="card">
- <div class="card-head">进行中的续标任务</div>
- <div class="card-body" style="padding:0" id="bid-task-list"></div>
- </div>
- <div class="card" id="bid-upload-card">
- <div class="card-head">快速开始</div>
- <div class="card-body">
- <p style="font-size:13px;color:var(--text2);margin-bottom:12px">从招标监测获取标书后,可在此继续编制或打开关联任务。</p>
- <button type="button" class="btn" onclick="navigate('tender-pool')">从招标监测选择 →</button>
- <button type="button" class="btn btn-primary" style="margin-left:8px" onclick="navigate('bid-task')">继续编制 →</button>
- </div>
- </div>
- </div>
- </section>
- <!-- ========== 标书编制详情 ========== -->
- <section id="view-bid-task" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#bid-workbench">← 标书编制</a></p>
- <h1 id="bid-task-title">—</h1>
- <p>上:原文对照 · 映射参考 · 预览 | 下:草稿编辑(≥1/3 页面)</p>
- </div>
- <div style="display:flex;gap:8px;align-items:center">
- <div class="progress-bar" style="width:120px;margin:0"><i id="bid-progress-bar" style="width:0%"></i></div>
- <button type="button" class="btn" id="btn-export-word" onclick="exportWord()" disabled title="全部确认后可导出">导出 Word</button>
- <button type="button" class="btn btn-primary" id="btn-register-bid" onclick="goRegisterBid()" disabled title="导出后可登记投标">登记投标</button>
- <button type="button" class="btn btn-primary" id="btn-confirm-all" onclick="confirmAllBlocks()">确认当前块</button>
- </div>
- </header>
- <div class="content bid-task-content">
- <div class="steps" id="bid-task-steps"></div>
- <div class="bid-summary-bar" id="bid-summary-bar"></div>
- <div class="bid-workspace">
- <div class="bid-col bid-col-doc">
- <div class="bid-col-head">招标文件 · 原文对照</div>
- <div class="bid-col-body" id="bid-doc-left"></div>
- </div>
- <div class="bid-col bid-col-ref">
- <div class="bid-col-head">拆解清单 & 数据映射</div>
- <div class="bid-col-body" id="bid-blocks-mid"></div>
- </div>
- <div class="bid-col bid-col-preview">
- <div class="bid-col-head">生成预览</div>
- <div class="bid-col-body preview-doc" id="bid-preview-right"></div>
- </div>
- <div class="bid-editor-zone" id="bid-editor-zone"></div>
- </div>
- </div>
- </section>
- <!-- ========== 开标与中标 ========== -->
- <section id="view-bid-result" class="view hidden">
- <header class="topbar">
- <div>
- <p style="font-size:12px"><a href="#market">← 市场拓展中心</a></p>
- <h1>开标与中标</h1>
- <p>投标登记 · 开标监控 · 结果抓取 · 中标推送</p>
- </div>
- <button type="button" class="btn" id="btn-simulate-result" onclick="simulateOpenBidResult()" style="display:none">模拟:开标结果发布</button>
- </header>
- <div class="content">
- <div class="page-tabs" id="result-tabs">
- <button type="button" class="page-tab active" data-tab="register" onclick="setResultTab('register')">投标登记</button>
- <button type="button" class="page-tab" data-tab="monitor" onclick="setResultTab('monitor')">开标监控 <span id="monitor-tab-badge"></span></button>
- <button type="button" class="page-tab" data-tab="win" onclick="setResultTab('win')">中标登记与推送</button>
- </div>
- <!-- 投标登记 -->
- <div id="result-panel-register">
- <div class="card">
- <div class="card-head">登记投标信息 · 开启开标监控</div>
- <div class="card-body">
- <p style="font-size:13px;color:var(--text2);margin-bottom:16px">标书提交后在此登记投标信息,系统将监控政府采购网开标结果页,结果发布后自动抓取并推送。</p>
- <div class="form-grid" id="register-form"></div>
- <div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">
- <button type="button" class="btn btn-primary" onclick="submitBidRegistration()">提交登记 · 开启监控</button>
- <button type="button" class="btn" onclick="prefillRegisterForm()">填充示例数据</button>
- </div>
- </div>
- </div>
- </div>
- <!-- 开标监控 -->
- <div id="result-panel-monitor" class="hidden">
- <div class="pool-stats" style="grid-template-columns:repeat(3,1fr);margin-bottom:16px" id="monitor-stats"></div>
- <div class="card">
- <div class="card-head">监控中的投标</div>
- <div class="card-body" id="monitor-list"></div>
- </div>
- </div>
- <!-- 中标登记与推送 -->
- <div id="result-panel-win" class="hidden">
- <div id="win-content"></div>
- </div>
- </div>
- </section>
- </div>
- </div>
- <!-- 招标详情抽屉 -->
- <div class="drawer-mask hidden" id="tender-drawer-mask" onclick="if(event.target===this)closeTenderDrawer()">
- <div class="drawer" onclick="event.stopPropagation()">
- <div class="drawer-head">
- <div><h2 style="font-size:16px" id="drawer-title">—</h2><p style="font-size:12px;color:var(--text3);margin-top:4px" id="drawer-sub">—</p></div>
- <button type="button" class="btn btn-ghost" onclick="closeTenderDrawer()">×</button>
- </div>
- <div class="drawer-body" id="drawer-body"></div>
- </div>
- </div>
- <div class="toast-wrap" id="toast-wrap"></div>
- <!-- ========== GIS 全屏 ========== -->
- <div id="view-gis" class="view-gis hidden">
- <div class="gis-top">
- <a class="gis-back" href="#dashboard">← 返回工作台</a>
- <h1>数据可视化平台 · 空间一张图</h1>
- <span style="font-size:11px;color:var(--text3)" id="gis-clock"></span>
- </div>
- <aside class="gis-left">
- <div class="search"><input type="search" placeholder="搜索项目、地址…" id="gis-search" /></div>
- <p class="count" id="gis-count">共 4 条</p>
- <div class="gis-list" id="gis-list"></div>
- </aside>
- <div class="map-mock">
- <div class="map-road r1"></div>
- <div class="map-road r2"></div>
- <div class="zone renew" id="gis-zone-renew" style="left:52%;top:42%;width:14%;height:11%"></div>
- <div class="pin-project" style="left:28%;top:38%" data-id="p001" title="青浦疾控中心"></div>
- <div class="pin-project renew" style="left:59%;top:47%" data-id="renewal-demo" title="青浦-公安局"></div>
- <div class="pin-project" style="left:35%;top:55%" data-id="p003" title="中山青浦分院"></div>
- <div class="pin-project" style="left:45%;top:30%" data-id="p004" title="徐泾枢纽"></div>
- </div>
- <div class="gis-tools">
- <strong style="display:block;margin-bottom:6px">图层</strong>
- <label><input type="checkbox" checked /> 项目点</label>
- <label><input type="checkbox" checked id="layer-renew" /> 合同即将到期</label>
- <label><input type="checkbox" /> 人员点</label>
- </div>
- <div class="gis-info-card hidden" id="gis-card">
- <header><h3 id="gis-card-title">—</h3><button type="button" class="btn btn-ghost" onclick="closeGisCard()">×</button></header>
- <dl id="gis-card-body"></dl>
- <div class="actions">
- <button type="button" class="btn" onclick="navigate('project')">项目详情</button>
- <button type="button" class="btn btn-primary" id="gis-btn-renewal">进入项目续标</button>
- </div>
- </div>
- </div>
- <script>
- const DEMO_TODAY = new Date('2026-06-08');
- const renewalDemo = {
- id: 'renewal-demo',
- region: '西',
- kingdeeCode: 'XMSQ0161',
- serialNo: 40,
- clientName: '上海市公安局青浦分局',
- name: '上海市公安局青浦分局物业保洁服务采购项目',
- displayName: '青浦-公安局',
- address: '青浦区范围',
- area: 170581,
- contractPeriod: '2025.06.20-2026.06.19',
- contractEnd: '2026-06-19',
- servicePeriod: '2025.06.20-2028.06.19,第1年',
- contractAmount: 16686132,
- annualContractAmount: 16686132,
- currentYearAmount: 7771623.12,
- estimatedAmount: 8914508.88,
- existingAmount: 16686132,
- newAmount: null,
- serviceScope: '保洁/绿化/维修/会务',
- contractStatusOnline: '已签',
- contractStatusOffline: '已签',
- staffCount: 68,
- leader: '李明华',
- tenderPlatform: '上海政府采购网',
- historyBids: [
- { year: 2023, result: '中标', amount: 6200000 },
- { year: 2020, result: '中标', amount: 5800000 },
- ],
- };
- const otherProjects = [
- { id: 'p001', kingdeeCode: 'XMSQ0102', displayName: '青浦-图书馆', name: '2026年青浦区图书馆物业管理项目', contractPeriod: '2026.02.28-2027.02.27', contractEnd: '2027-02-27', serviceScope: '全委', contractStatusOnline: '已签' },
- { id: 'p003', kingdeeCode: 'XMSQ0104', displayName: '青浦-司法中心', name: '司法中心物业管理服务项目', contractPeriod: '2026.01.29-2027.01.28', contractEnd: '2027-01-28', serviceScope: '全委', contractStatusOnline: '已签' },
- { id: 'p004', kingdeeCode: 'XMSQ0108', displayName: '青浦-税务局', name: '国家税务总局上海市青浦区税务局物业管理服务', contractPeriod: '2026.01.01-2026.12.31', contractEnd: '2026-12-31', serviceScope: '全委', contractStatusOnline: '已签' },
- ];
- let tenderPool = [
- {
- id: 't-renewal', title: '上海市公安局青浦分局物业保洁服务采购项目公开招标公告',
- source: '上海政府采购网', district: '青浦区', publishDate: '2026-06-05', deadline: '2026-06-25',
- budget: 16686132, type: 'renewal', matchScore: 98, linkedProjectId: 'renewal-demo',
- tenderNo: 'SH-GP-2026-0892',
- sourceUrl: 'https://www.zfcg.sh.gov.cn/site/detail?articleId=SH-GP-2026-0892',
- status: 'monitoring', keywords: ['保洁', '绿化', '青浦', '公安局'], convertedTo: null,
- content: '采购人:上海市公安局青浦分局。年化合同额约 1,668.61 万元。服务期限三年。获取招标文件时间:2026年6月5日至6月20日。',
- },
- {
- id: 't-002', title: '青浦区某学校物业管理服务采购项目招标公告',
- source: '上海政府采购网', district: '青浦区', publishDate: '2026-06-07', deadline: '2026-06-28',
- budget: 3200000, type: 'new', matchScore: 72, linkedProjectId: null,
- tenderNo: 'SH-GP-2026-0915',
- sourceUrl: 'https://www.zfcg.sh.gov.cn/site/detail?articleId=SH-GP-2026-0915',
- status: 'new', keywords: ['学校', '公建物业'], convertedTo: null,
- content: '采购人:青浦区教育局下属学校。预算320万元/年。',
- },
- {
- id: 't-003', title: '徐汇区政务中心保洁保安外包服务招标公告',
- source: '上海政府采购网', district: '徐汇区', publishDate: '2026-06-06', deadline: '2026-06-22',
- budget: 4500000, type: 'new', matchScore: 65, linkedProjectId: null,
- tenderNo: 'SH-GP-2026-0908',
- sourceUrl: 'https://www.zfcg.sh.gov.cn/site/detail?articleId=SH-GP-2026-0908',
- status: 'read', keywords: ['政务中心', '保洁保安'], convertedTo: 'reserve',
- content: '采购人:徐汇区机关事务管理局。',
- },
- {
- id: 't-004', title: '青浦区某医院后勤保障服务公开招标公告',
- source: '中国政府采购网', district: '青浦区', publishDate: '2026-06-04', deadline: '2026-06-18',
- budget: 8900000, type: 'new', matchScore: 81, linkedProjectId: null,
- tenderNo: 'CCGP-2026-QP-0621',
- sourceUrl: 'https://www.ccgp.gov.cn/ccgp/shanghai/site/detail?articleId=CCGP-2026-QP-0621',
- status: 'read', keywords: ['医院', '后勤保障'], convertedTo: 'reserve',
- content: '采购人:青浦区某公立医院。含运送、保洁、工程维保。',
- },
- {
- id: 't-005', title: '青浦区练塘镇社区物业综合管理服务采购公告',
- source: '上海政府采购网', district: '青浦区', publishDate: '2026-06-03', deadline: '2026-06-15',
- budget: 1800000, type: 'new', matchScore: 58, linkedProjectId: null,
- tenderNo: 'SH-GP-2026-0886',
- sourceUrl: 'https://www.zfcg.sh.gov.cn/site/detail?articleId=SH-GP-2026-0886',
- status: 'read', keywords: ['社区物业', '练塘'], convertedTo: null,
- content: '采购人:练塘镇人民政府。',
- },
- ];
- let bidPipeline = [];
- let reserveProjects = [
- {
- id: 'rs-1', tenderId: 't-004',
- title: '青浦区某医院后勤保障服务公开招标公告',
- district: '青浦区', source: '中国政府采购网', budget: 8900000, matchScore: 81,
- convertedAt: '2026-06-06', deadline: '2026-06-18', status: 'evaluating',
- reason: '医院后勤业态匹配,预算890万/年,建议重点评估',
- owner: '市场部 · 张敏',
- },
- {
- id: 'rs-2', tenderId: 't-003',
- title: '徐汇区政务中心保洁保安外包服务招标公告',
- district: '徐汇区', source: '上海政府采购网', budget: 4500000, matchScore: 65,
- convertedAt: '2026-06-05', deadline: '2026-06-22', status: 'watching',
- reason: '跨区域项目,匹配度一般,持续观察',
- owner: '市场部 · 李强',
- },
- ];
- const bidBlocks = [
- {
- id: 'b1', title: '评分标准', type: 'auto', source: '历史中标对照 · 2023版', docRef: '第四章 评标办法', docLine: 7,
- requirements: ['价格分 30 分', '技术分 50 分', '商务分 10 分', '信誉分 10 分'],
- mappings: [
- { field: '评分结构', source: '2023中标标书', value: '与历史一致', status: 'ok' },
- { field: '价格分计算', source: '财务模板', value: '最低价法', status: 'ok' },
- ],
- draft: '评标办法响应:完全响应招标文件第四章评分标准,价格分采用最低价法,技术方案按分项得分…',
- historyNote: '2023版沿用评分结构,本次无重大调整',
- scores: [{ label: '价格分', pts: 30 }, { label: '技术分', pts: 50 }, { label: '商务信誉', pts: 20 }],
- },
- {
- id: 'b2', title: '投标人资质要求', type: 'auto', source: '综合办 · 证照库', docRef: '第二章 投标人须知', docLine: 4,
- requirements: ['物业一级资质', 'ISO9001 质量管理体系', '近3年无重大违法记录'],
- mappings: [
- { field: '营业执照', source: '综合办证照库', value: '91440300MA5XXXX', status: 'ok' },
- { field: '物业资质', source: '综合办证照库', value: '一级 · 有效期至2027-12', status: 'ok' },
- { field: 'ISO9001', source: '综合办证照库', value: '已认证', status: 'ok' },
- ],
- draft: '资质证明:营业执照、物业一级资质证书、ISO9001 认证证书均已自动关联…',
- historyNote: '证照库已更新,无需人工补充',
- },
- {
- id: 'b3', title: '项目管理机构', type: 'confirm', source: '人事 · 花名册 + 证书', docRef: '第五章 人员配置', docLine: 9,
- requirements: ['项目经理 1 名 · 持证上岗', '安保主管 1 名', '工程主管 1 名', '保洁主管 1 名'],
- mappings: [
- { field: '项目经理', source: '人事花名册', value: '李明华 · 物业师证', status: 'ok' },
- { field: '安保主管', source: '人事花名册', value: '王强 · 保安员证', status: 'ok' },
- { field: '工程主管', source: '人事花名册', value: '张伟 · 电工证', status: 'warn' },
- { field: '保洁主管', source: '人事花名册', value: '待确认人选', status: 'warn' },
- ],
- draft: '项目管理机构:项目经理李明华(10年机关物业经验),安保主管王强,工程主管张伟…保洁主管岗位待确认后补充。',
- historyNote: '2023版项目经理为同一人,机构稳定性加分',
- },
- {
- id: 'b4', title: '类似项目业绩', type: 'auto', source: '市场部 · 历史中标库', docRef: '第六章 业绩证明', docLine: 5,
- requirements: ['近3年类似机关物业项目 ≥ 2 个', '单个合同额 ≥ 500 万/年'],
- mappings: [
- { field: '业绩1', source: '历史中标库', value: '2023 青浦公安分局 · 620万', status: 'ok' },
- { field: '业绩2', source: '历史中标库', value: '2022 青浦图书馆 · 480万', status: 'ok' },
- { field: '业绩3', source: '历史中标库', value: '2021 青浦司法中心 · 350万', status: 'ok' },
- ],
- draft: '类似业绩:青浦公安分局物业保洁(2023,620万/年)、青浦图书馆物业管理(2022,480万/年)…',
- historyNote: '本项目即为2023中标续标,业绩高度匹配',
- },
- {
- id: 'b5', title: '服务方案与技术响应', type: 'manual', source: 'AI 草稿 + 人工确认', docRef: '第七章 技术方案', docLine: 11,
- requirements: ['安保服务标准与巡逻方案', '保洁频次与质量标准', '工程维保响应时限', '绿化养护方案', '会务保障流程'],
- mappings: [
- { field: '安保方案', source: 'AI 草稿', value: '已生成 · 待确认', status: 'warn' },
- { field: '保洁方案', source: 'AI 草稿', value: '已生成 · 待确认', status: 'warn' },
- { field: '工程维保', source: '历史标书+AI', value: '已生成 · 待确认', status: 'warn' },
- ],
- draft: '一、安保服务\n24小时门岗值守,每2小时巡逻一次,突发事件5分钟内响应…\n\n二、保洁服务\n办公区每日2次保洁,卫生间每2小时巡检…\n\n三、工程维保\n一般报修2小时内到场,紧急报修30分钟内响应…',
- historyNote: 'AI 基于2023中标标书+本项目服务范围生成,需人工核对机关特殊要求',
- },
- {
- id: 'b6', title: '商务报价结构', type: 'confirm', source: '财务区间 + 历史报价', docRef: '第八章 报价格式', docLine: 3,
- requirements: ['报价格式符合招标文件要求', '分项报价:人工/材料/管理费', '不超过预算上限 1,668.61 万'],
- mappings: [
- { field: '总报价', source: '财务区间', value: '1,620 万/年(建议)', status: 'warn' },
- { field: '人工成本', source: 'HR定编', value: '68人 · 920万', status: 'ok' },
- { field: '管理费', source: '财务模板', value: '8%', status: 'ok' },
- { field: '2023报价', source: '历史中标', value: '620万 → 本次建议1620万', status: 'warn' },
- ],
- draft: '商务报价:年化总报价 1,620 万元,其中人工成本 920 万、材料及耗材 380 万、管理费 8%…',
- historyNote: '2023中标620万,本次预算上限1668万,建议报价1620万留竞争空间',
- },
- ];
- const tenderDocLines = [
- '第一章 招标公告',
- ' 1.1 项目概况:机关办公区物业管理',
- ' 1.2 预算金额:年化约 1,668 万元',
- '第二章 投标人须知',
- ' 2.1 资质要求:物业一级资质…',
- ' 2.2 业绩要求:近3年类似项目…',
- '第四章 评标办法',
- ' 4.1 价格分 30 分 · 技术分 50 分…',
- '第五章 人员配置要求',
- ' 5.1 项目经理 1 名,持证上岗…',
- '第七章 技术方案要求',
- ' 7.1 安保、保洁、工程服务标准…',
- ];
- const renewalCatalog = [
- { id: 'renewal-demo', region: '西', kingdeeCode: 'XMSQ0161', clientName: '上海市公安局青浦分局', displayName: '青浦-公安局', name: '上海市公安局青浦分局物业保洁服务采购项目', contractEnd: '2026-06-19', contractPeriod: '2025.06.20-2026.06.19', annualContractAmount: 16686132, serviceScope: '保洁/绿化/维修/会务', listStatus: 'active' },
- { id: 'p004', region: '西', kingdeeCode: 'XMSQ0108', clientName: '国家税务总局上海市青浦区税务局', displayName: '青浦-税务局', name: '国家税务总局上海市青浦区税务局物业管理服务', contractEnd: '2026-12-31', contractPeriod: '2026.01.01-2026.12.31', annualContractAmount: 4200000, serviceScope: '全委', listStatus: 'prep' },
- { id: 'p003', region: '西', kingdeeCode: 'XMSQ0104', clientName: '青浦区司法中心', displayName: '青浦-司法中心', name: '司法中心物业管理服务项目', contractEnd: '2027-01-28', contractPeriod: '2026.01.29-2027.01.28', annualContractAmount: 3500000, serviceScope: '全委', listStatus: 'watch' },
- { id: 'p001', region: '西', kingdeeCode: 'XMSQ0102', clientName: '青浦区图书馆', displayName: '青浦-图书馆', name: '2026年青浦区图书馆物业管理项目', contractEnd: '2027-02-27', contractPeriod: '2026.02.28-2027.02.27', annualContractAmount: 4800000, serviceScope: '全委', listStatus: 'watch' },
- { id: 'p-done', region: '东', kingdeeCode: 'XMSQ0088', clientName: '徐汇区机关事务管理局', displayName: '徐汇-政务中心', name: '徐汇区政务中心物业管理服务', contractEnd: '2025-12-31', contractPeriod: '2023.01.01-2025.12.31', annualContractAmount: 5100000, serviceScope: '全委', listStatus: 'completed', completedDate: '2025-11' },
- ];
- const appState = {
- poolFilter: 'all',
- bidPipelineFilter: 'all',
- reserveFilter: 'all',
- renewalFilter: 'all',
- selectedRenewalId: 'renewal-demo',
- selectedTenderId: null,
- activeBlockId: 'b3',
- confirmedBlocks: new Set(),
- blockDrafts: {},
- renewalTasks: [
- { id: 'r1', text: '客户回访与续标意向确认', done: true, action: null },
- { id: 'r2', text: '现场踏勘与服务评估', done: true, action: null },
- { id: 'r3', text: '关注招标公告发布(监控中)', done: false, action: 'renewal' },
- { id: 'r4', text: '报名并获取招标文件', done: false, action: 'renewal-fetch' },
- { id: 'r5', text: '标书需求拆解与生成', done: false, action: 'bid-task' },
- { id: 'r6', text: '投标登记与开标监控', done: false, action: 'bid-result' },
- { id: 'r7', text: '中标确认与部门推送', done: false, action: 'bid-result-win' },
- ],
- bidTaskFromTender: false,
- gisSelectedId: 'renewal-demo',
- registerFormBuilt: false,
- bidExported: false,
- resultTab: 'register',
- bidSubmissions: [],
- selectedSubmissionId: null,
- };
- function daysUntil(dateStr) {
- return Math.ceil((new Date(dateStr) - DEMO_TODAY) / 86400000);
- }
- function formatMoney(n) {
- if (n == null || n === '' || Number.isNaN(Number(n))) return '—';
- if (n >= 10000) return (n / 10000).toFixed(2) + ' 万';
- return n + ' 元';
- }
- function formatYuan(n) {
- if (n == null || n === '' || Number.isNaN(Number(n))) return '—';
- return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + ' 元';
- }
- function renderProjectFields(p) {
- document.getElementById('project-base').innerHTML = `
- <dt>区域</dt><dd>${p.region ?? '—'}</dd>
- <dt>金蝶编码</dt><dd>${p.kingdeeCode ?? '—'}</dd>
- <dt>序号</dt><dd>${p.serialNo ?? '—'}</dd>
- <dt>甲方名称</dt><dd>${p.clientName ?? '—'}</dd>
- <dt>项目名称</dt><dd>${p.name ?? '—'}</dd>
- <dt>项目简称</dt><dd>${p.displayName ?? '—'}</dd>
- <dt>项目地址</dt><dd>${p.address ?? '—'}</dd>
- <dt>建筑面积</dt><dd>${p.area != null ? p.area.toLocaleString('zh-CN') + ' m²' : '—'}</dd>
- `;
- document.getElementById('project-contract').innerHTML = `
- <dt>合同起止日期</dt><dd>${p.contractPeriod ?? '—'}</dd>
- <dt>服务期限</dt><dd>${p.servicePeriod ?? '—'}</dd>
- <dt>在管服务内容</dt><dd>${p.serviceScope ?? '—'}</dd>
- <dt>合同状况(线上)</dt><dd>${p.contractStatusOnline ? `<span class="tag">${p.contractStatusOnline}</span>` : '—'}</dd>
- <dt>合同状况(线下)</dt><dd>${p.contractStatusOffline ? `<span class="tag">${p.contractStatusOffline}</span>` : '—'}</dd>
- `;
- document.getElementById('project-finance').innerHTML = `
- <dt>合同金额</dt><dd>${formatYuan(p.contractAmount)}</dd>
- <dt>年化合同额</dt><dd>${formatYuan(p.annualContractAmount)}</dd>
- <dt>2026年当期年化合同额</dt><dd>${formatYuan(p.currentYearAmount)}</dd>
- <dt>预估合同额</dt><dd>${formatYuan(p.estimatedAmount)}</dd>
- <dt>存量合同额</dt><dd>${formatYuan(p.existingAmount)}</dd>
- <dt>新增合同额</dt><dd>${formatYuan(p.newAmount)}</dd>
- `;
- }
- function fillTypeLabel(type) {
- return { auto: ['🟢 自动', 'fill-auto'], confirm: ['🟡 需确认', 'fill-confirm'], manual: ['🔴 人工撰写', 'fill-manual'] }[type];
- }
- function toast(msg) {
- const wrap = document.getElementById('toast-wrap');
- const el = document.createElement('div');
- el.className = 'toast';
- el.textContent = msg;
- wrap.appendChild(el);
- setTimeout(() => el.remove(), 3200);
- }
- function getRenewalTender() {
- return tenderPool.find(t => t.id === 't-renewal');
- }
- function simulateTenderMatch() {
- const t = getRenewalTender();
- if (t) {
- t.status = 'matched';
- t.publishDate = '2026-06-08';
- appState.renewalTasks.find(x => x.id === 'r3').done = true;
- toast('续标监控匹配成功:青浦-公安局 招标公告已发布');
- renderTenderPool();
- renderDashboard();
- renderProject();
- renderMarket();
- if (location.hash === '#renewal-detail') renderRenewalHub();
- }
- }
- function renderDashboard() {
- const days = daysUntil(renewalDemo.contractEnd);
- const t = getRenewalTender();
- const matched = t && t.status === 'matched';
- document.getElementById('dashboard-stats').innerHTML = `
- <div class="stat clickable" onclick="navigate('projects')"><label>在管项目</label><b>108</b><small>本年新增 6</small></div>
- <div class="stat clickable" onclick="navigate('projects')"><label>在岗人员</label><b>3286</b><small>编制满足率 94%</small></div>
- <div class="stat"><label>年化收入</label><b>2.86亿</b><small>预算执行 87%</small></div>
- <div class="stat"><label>平均毛利率</label><b>10.8%</b><small>低于目标 3 项</small></div>
- <div class="stat warn clickable" onclick="navigate('project')"><label>待续约</label><b>1</b><small>3个月内到期</small></div>
- <div class="stat warn clickable" onclick="navigate('tender-pool')"><label>招标信息</label><b>${matched ? '1' : '3'}</b><small>${matched ? '续标已匹配' : '今日新增'} →</small></div>
- `;
- document.getElementById('todo-list').innerHTML = `
- <li class="todo-item">
- <span class="todo-dot urgent"></span>
- <div><a href="#renewal-detail"><strong>${renewalDemo.displayName}</strong> 续标进行中 · 合同剩 ${days} 天 → 进入续标详情</a></div>
- </li>
- <li class="todo-item">
- <span class="todo-dot ${appState.bidSubmissions.some(s => s.status === 'result_fetched') ? 'urgent' : ''}"></span>
- <div><a href="#renewal-detail">${appState.bidSubmissions.some(s => s.status === 'result_fetched') ? '【开标结果】已中标,请完成登记推送' : appState.bidSubmissions.some(s => s.status === 'monitoring') ? '开标监控中 · 距开标 ' + daysUntilOpen(appState.bidSubmissions[0]) + ' 天' : matched ? '招标公告已匹配 · 请获取标书' : '项目续标 · 招标公告监控中'}</a></div>
- </li>
- <li class="todo-item"><span class="todo-dot"></span><div>练塘镇物业结项报告待审核</div></li>
- <li class="todo-item"><span class="todo-dot"></span><div>疾控中心项目缺编 2 名保洁</div></li>
- `;
- document.getElementById('recent-projects').innerHTML = [renewalDemo, ...otherProjects].map(p => {
- const d = daysUntil(p.contractEnd);
- const isDemo = p.id === renewalDemo.id;
- return `<tr onclick="navigate('project')" style="cursor:pointer">
- <td><strong>${p.displayName}</strong><div style="font-size:11px;color:var(--text3)">${p.kingdeeCode || ''}</div></td>
- <td>${p.serviceScope || '—'}</td>
- <td>${p.contractPeriod || p.contractEnd}${isDemo ? ` <span class="tag tag-danger">${d}天</span>` : ''}</td>
- <td>${p.contractStatusOnline || '—'}</td>
- <td>${isDemo ? '<span class="tag tag-warn">续标预警</span>' : '<span class="tag">在管</span>'}</td>
- </tr>`;
- }).join('');
- }
- function renderProjectList() {
- document.getElementById('project-list').innerHTML = [renewalDemo, ...otherProjects].map(p => {
- const d = daysUntil(p.contractEnd);
- return `<tr>
- <td><a href="#project">${p.displayName}</a></td>
- <td>${p.kingdeeCode || '—'}</td>
- <td>${p.contractPeriod || p.contractEnd}</td>
- <td>${d > 180 ? d + ' 天' : `<span class="tag tag-warn">${d} 天</span>`}</td>
- <td><a href="#project">查看</a></td>
- </tr>`;
- }).join('');
- }
- function getRenewalStage() {
- const t = getRenewalTender();
- const matched = t?.status === 'matched';
- const hasBid = appState.bidTaskFromTender;
- const bidDone = appState.confirmedBlocks.size === bidBlocks.length;
- const exported = appState.bidExported;
- const submitted = appState.bidSubmissions.some(s => s.projectId === renewalDemo.id);
- const monitoring = appState.bidSubmissions.find(s => s.projectId === renewalDemo.id && s.status === 'monitoring');
- const resultFetched = appState.bidSubmissions.find(s => s.projectId === renewalDemo.id && s.status === 'result_fetched');
- const pushed = appState.bidSubmissions.some(s => s.projectId === renewalDemo.id && s.status === 'pushed');
- if (pushed) return 7;
- if (resultFetched) return 7;
- if (monitoring || submitted) return 6;
- if (exported) return 5;
- if (bidDone || hasBid) return 4;
- if (matched) return 4;
- if (appState.renewalTasks.find(x => x.id === 'r2').done) return 3;
- return 2;
- }
- function getRenewalProject(id) {
- if (id === 'renewal-demo') return renewalDemo;
- const cat = renewalCatalog.find(x => x.id === id);
- if (!cat) return renewalDemo;
- return { ...cat, ...(otherProjects.find(p => p.id === id) || {}) };
- }
- function getRenewalListStatus(p) {
- if (p.listStatus === 'completed') return { label: '已完成', tag: 'tag', days: null };
- if (p.id === 'renewal-demo' && p.listStatus === 'active') {
- const stageLabels = ['', '预警触发', '续标准备', '招标监控', '标书制作', '投标提交', '开标监控', '中标推送'];
- return { label: '进行中 · ' + stageLabels[getRenewalStage()], tag: 'tag-warn', days: daysUntil(p.contractEnd) };
- }
- const days = daysUntil(p.contractEnd);
- if (days <= 90) return { label: '90天内到期', tag: 'tag-danger', days };
- if (days <= 180) return { label: '续标准备', tag: 'tag-warn', days };
- return { label: '监控中', tag: 'tag', days };
- }
- function setRenewalFilter(f) {
- appState.renewalFilter = f;
- renderRenewalList();
- }
- function renderRenewalList() {
- const search = (document.getElementById('renewal-search')?.value || '').trim().toLowerCase();
- const region = document.getElementById('renewal-region')?.value || '';
- const filter = appState.renewalFilter;
- let items = renewalCatalog.map(p => ({ ...p, ...(p.id === 'renewal-demo' ? renewalDemo : otherProjects.find(x => x.id === p.id) || {}) }));
- if (search) items = items.filter(p => (p.displayName + p.clientName + p.name).toLowerCase().includes(search));
- if (region) items = items.filter(p => p.region === region);
- if (filter === 'active') items = items.filter(p => p.listStatus === 'active');
- else if (filter === 'urgent') items = items.filter(p => p.listStatus !== 'completed' && daysUntil(p.contractEnd) <= 90);
- else if (filter === 'prep') items = items.filter(p => p.listStatus === 'prep' || p.listStatus === 'watch');
- else if (filter === 'completed') items = items.filter(p => p.listStatus === 'completed');
- const active = renewalCatalog.filter(p => p.listStatus === 'active').length;
- const urgent = renewalCatalog.filter(p => p.listStatus !== 'completed' && daysUntil(p.contractEnd) <= 90).length;
- const prep = renewalCatalog.filter(p => p.listStatus === 'prep' || p.listStatus === 'watch').length;
- const done = renewalCatalog.filter(p => p.listStatus === 'completed').length;
- document.getElementById('renewal-stats').innerHTML = `
- <div class="pool-stat ${filter === 'all' ? 'highlight' : ''}" onclick="setRenewalFilter('all')"><span>全部</span><b>${renewalCatalog.length}</b></div>
- <div class="pool-stat ${filter === 'active' ? 'highlight' : ''}" onclick="setRenewalFilter('active')"><span>进行中</span><b>${active}</b></div>
- <div class="pool-stat ${filter === 'urgent' ? 'highlight' : ''}" onclick="setRenewalFilter('urgent')"><span>90天内到期</span><b>${urgent}</b></div>
- <div class="pool-stat ${filter === 'completed' ? 'highlight' : ''}" onclick="setRenewalFilter('completed')"><span>已完成</span><b>${done}</b></div>
- `;
- document.getElementById('renewal-chips').innerHTML = [
- ['all', '全部'], ['active', '进行中'], ['urgent', '90天内'], ['prep', '续标准备'], ['completed', '已完成'],
- ].map(([k, label]) => `<button type="button" class="chip ${filter === k ? 'active' : ''}" onclick="setRenewalFilter('${k}')">${label}</button>`).join('');
- if (!items.length) {
- document.getElementById('renewal-list').innerHTML = `<div class="empty-shell" style="border:none;padding:32px"><strong>暂无匹配项目</strong></div>`;
- return;
- }
- document.getElementById('renewal-list').innerHTML = items.map(p => {
- const st = getRenewalListStatus(p);
- const daysCls = st.days != null && st.days <= 30 ? '' : st.days != null && st.days <= 90 ? 'warn' : st.days != null ? 'ok' : '';
- return `<div class="renewal-row" onclick="openRenewalDetail('${p.id}')">
- <div>
- <div class="renewal-row-title">${p.displayName}</div>
- <div class="renewal-row-meta">
- <span>${p.clientName || p.displayName}</span>
- <span>合同 ${p.contractPeriod || p.contractEnd}</span>
- <span>年化 ${formatMoney(p.annualContractAmount)}</span>
- <span>${p.serviceScope || '—'}</span>
- </div>
- </div>
- <div class="renewal-row-actions">
- ${st.days != null ? `<span class="days-badge ${daysCls}">剩 ${st.days} 天</span>` : ''}
- <span class="tag ${st.tag}">${st.label}</span>
- <button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="event.stopPropagation();openRenewalDetail('${p.id}')">进入 →</button>
- </div>
- </div>`;
- }).join('');
- }
- function openRenewalDetail(id) {
- appState.selectedRenewalId = id;
- navigate('renewal-detail');
- }
- function renderRenewalHub() {
- const pid = appState.selectedRenewalId || 'renewal-demo';
- if (pid !== 'renewal-demo') {
- renderRenewalDetailSimple(pid);
- return;
- }
- const p = renewalDemo;
- const days = daysUntil(p.contractEnd);
- const t = getRenewalTender();
- const matched = t?.status === 'matched';
- const stage = getRenewalStage();
- const sub = appState.bidSubmissions.find(s => s.projectId === p.id);
- document.getElementById('renewal-page-title').textContent = p.displayName + ' · 项目续标';
- document.getElementById('renewal-page-sub').textContent = p.address + ' · 合同到期 ' + p.contractEnd;
- const steps = [
- { n: 1, label: '预警触发' },
- { n: 2, label: '续标准备' },
- { n: 3, label: '招标监控' },
- { n: 4, label: '标书制作' },
- { n: 5, label: '投标提交' },
- { n: 6, label: '开标监控' },
- { n: 7, label: '中标推送' },
- ];
- const stepperHtml = steps.map((s, i) => {
- const cls = s.n < stage ? 'done' : s.n === stage ? 'active' : '';
- const arrow = i < steps.length - 1 ? '<span class="renewal-step-arrow">→</span>' : '';
- return `<div class="renewal-step ${cls}"><span class="renewal-step-dot">${s.n < stage ? '✓' : s.n}</span>${s.label}</div>${arrow}`;
- }).join('');
- const actionMap = {
- 2: { title: '完成续标准备', desc: '确认客户回访与现场踏勘已完成,准备进入招标监控阶段。', primary: '标记准备完成', primaryFn: 'completeRenewalPrep()', secondary: null },
- 3: { title: '监控招标公告', desc: `系统正在监控 ${p.tenderPlatform},匹配本项目续标公告。`, primary: matched ? '查看匹配公告' : '模拟:公告发布', primaryFn: matched ? 'scrollRenewalTender()' : 'simulateTenderMatch()', secondary: '招标监测', secondaryFn: "navigate('tender-pool')" },
- 4: { title: matched ? '制作续标投标文件' : '等待招标公告', desc: matched ? '招标文件已就绪,进入标书编制完成拆解与生成。' : '公告发布后将自动关联本项目,届时可获取招标文件。', primary: matched ? '进入标书编制' : '模拟公告发布', primaryFn: matched ? "navigate('bid-task')" : 'simulateTenderMatch()', secondary: matched ? '获取标书' : null, secondaryFn: matched ? "fetchTenderFromRenewal()" : null },
- 5: { title: '提交投标并登记', desc: '标书完成后导出 Word,登记投标信息以便开标监控。', primary: appState.bidExported ? '登记投标' : '继续制作标书', primaryFn: appState.bidExported ? "navigate('bid-result')" : "navigate('bid-task')", secondary: null },
- 6: { title: '开标监控中', desc: sub ? `已登记投标 · 开标 ${sub.openBidDate} ${sub.openBidTime} · 距开标 ${daysUntilOpen(sub)} 天` : '请先完成投标登记', primary: sub?.status === 'monitoring' ? '模拟:开标结果发布' : '登记投标', primaryFn: sub?.status === 'monitoring' ? "simulateOpenBidResult('" + sub.id + "')" : "navigate('bid-result')", secondary: '查看监控详情', secondaryFn: "navigate('bid-result');setResultTab('monitor')" },
- 7: { title: sub?.status === 'pushed' ? '续标流程已完成' : '确认中标并推送', desc: sub?.status === 'pushed' ? '已推送运营、HR、财务、条线,等待前介启动。' : '开标结果已出,请确认中标信息并推送至各职能部门。', primary: sub?.status === 'pushed' ? '查看推送记录' : '中标登记与推送', primaryFn: sub?.status === 'pushed' ? "navigate('bid-result');setResultTab('win')" : "navigate('bid-result');setResultTab('win')", secondary: null },
- };
- const action = actionMap[stage] || actionMap[3];
- document.getElementById('renewal-header-actions').innerHTML = `
- <button type="button" class="btn" onclick="navigate('gis')">地图</button>
- <button type="button" class="btn" onclick="navigate('project')">项目档案</button>
- `;
- const tenderMini = t ? `
- <div class="tender-mini ${matched ? 'matched' : 'monitoring'}" id="renewal-tender-card">
- <strong style="font-size:13px">${matched ? '● 已匹配公告' : '○ 监控中'}</strong>
- <p style="font-size:12px;color:var(--text2);margin:8px 0">${t.title}</p>
- <div style="font-size:11px;color:var(--text3)">发布 ${t.publishDate} · 截止 ${t.deadline} · 匹配 ${t.matchScore} 分</div>
- <div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
- ${matched ? `<button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="fetchTenderFromRenewal()">获取标书</button>` : `<button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="simulateTenderMatch()">模拟发布</button>`}
- <button type="button" class="btn btn-ghost" style="font-size:12px;padding:4px 10px" onclick="openTenderDrawer('t-renewal')">详情</button>
- </div>
- </div>` : '<p style="color:var(--text3);font-size:12px">暂无关联招标信息</p>';
- document.getElementById('renewal-content').innerHTML = `
- <div class="renewal-hero">
- <div>
- <h2>${p.displayName}</h2>
- <p>${p.name}</p>
- <p style="margin-top:8px;font-size:12px;opacity:0.85">${p.clientName} · ${p.serviceScope} · 年化 ${formatMoney(p.annualContractAmount)}</p>
- </div>
- <div class="renewal-countdown">
- <b>${days}</b>
- <span>天后合同到期</span>
- </div>
- </div>
- <div class="renewal-stepper">${stepperHtml}</div>
- <div class="renewal-layout">
- <div class="renewal-main">
- <div class="action-card">
- <h3>${action.title}</h3>
- <p>${action.desc}</p>
- <div class="actions">
- <button type="button" class="btn btn-primary" onclick="${action.primaryFn}">${action.primary}</button>
- ${action.secondary ? `<button type="button" class="btn" onclick="${action.secondaryFn}">${action.secondary}</button>` : ''}
- </div>
- </div>
- <div class="card">
- <div class="card-head">续标任务清单</div>
- <div class="card-body"><ul class="checklist" id="renewal-hub-checklist"></ul></div>
- </div>
- <div class="card">
- <div class="card-head">历史投标参考</div>
- <div class="card-body">
- <table>
- <thead><tr><th>年份</th><th>结果</th><th>金额</th><th>操作</th></tr></thead>
- <tbody>${p.historyBids.map(b => `<tr><td>${b.year}</td><td>${b.result}</td><td>${formatMoney(b.amount)}</td><td><a href="#" onclick="toast('历史标书档案接入后开放');return false">查看标书</a></td></tr>`).join('')}</tbody>
- </table>
- </div>
- </div>
- </div>
- <div class="renewal-side">
- <div class="side-card">
- <div class="side-card-head">项目摘要</div>
- <div class="side-card-body">
- <dl class="info info-wide" style="grid-template-columns:96px 1fr">
- <dt>金蝶编码</dt><dd>${p.kingdeeCode}</dd>
- <dt>合同起止</dt><dd>${p.contractPeriod}</dd>
- <dt>年化合同额</dt><dd>${formatYuan(p.annualContractAmount)}</dd>
- <dt>在管服务内容</dt><dd>${p.serviceScope}</dd>
- <dt>合同状况</dt><dd>${p.contractStatusOnline} / ${p.contractStatusOffline}</dd>
- </dl>
- </div>
- </div>
- <div class="side-card">
- <div class="side-card-head">本项目招标动态</div>
- <div class="side-card-body">${tenderMini}</div>
- </div>
- <div class="side-card">
- <div class="side-card-head">关联模块</div>
- <div class="side-card-body" style="display:flex;flex-direction:column;gap:8px">
- <button type="button" class="btn" style="width:100%;text-align:left" onclick="navigate('bid-task')">标书编制</button>
- <button type="button" class="btn" style="width:100%;text-align:left" onclick="navigate('bid-result')">开标与中标</button>
- <button type="button" class="btn btn-ghost" style="width:100%;text-align:left;font-size:12px" onclick="navigate('tender-pool')">查看全部招标信息 →</button>
- </div>
- </div>
- <div class="side-card">
- <div class="side-card-head">流程记录</div>
- <div class="side-card-body"><ul class="timeline">${buildRenewalTimeline().map(x => `<li class="${x.done ? 'done' : ''}">${x.text}</li>`).join('')}</ul></div>
- </div>
- </div>
- </div>
- `;
- document.getElementById('renewal-hub-checklist').innerHTML = appState.renewalTasks.map(t =>
- `<li class="${t.done ? 'done' : ''} clickable" onclick="onRenewalChecklistClick('${t.id}')">
- <span>${t.done ? '✓' : '○'}</span> ${t.text}
- </li>`
- ).join('');
- }
- function renderRenewalDetailSimple(id) {
- const p = getRenewalProject(id);
- const days = p.contractEnd ? daysUntil(p.contractEnd) : '—';
- const st = getRenewalListStatus(p);
- document.getElementById('renewal-page-title').textContent = p.displayName + ' · 项目续标';
- document.getElementById('renewal-page-sub').textContent = (p.clientName || p.displayName) + ' · 合同到期 ' + (p.contractEnd || '—');
- document.getElementById('renewal-header-actions').innerHTML = `
- <button type="button" class="btn" onclick="navigate('project')">项目档案</button>
- `;
- document.getElementById('renewal-content').innerHTML = `
- <div class="banner-renewal ${days !== '—' && days <= 90 ? 'urgent' : ''}">
- <div>
- <strong>${st.label}</strong> · 合同 ${p.contractPeriod || p.contractEnd}
- ${days !== '—' ? `<div style="font-size:12px;color:var(--text2);margin-top:6px">距本期到期 <strong>${days}</strong> 天 · 年化 ${formatMoney(p.annualContractAmount)}</div>` : ''}
- </div>
- ${days !== '—' && days <= 180 ? `<button type="button" class="btn btn-primary" onclick="toast('系统将自动监控招标公告,到期前90天启动续标流程')">加入续标监控</button>` : ''}
- </div>
- <div class="card">
- <div class="card-head">项目信息</div>
- <div class="card-body">
- <dl class="info info-wide">
- <dt>金蝶编码</dt><dd>${p.kingdeeCode || '—'}</dd>
- <dt>甲方名称</dt><dd>${p.clientName || '—'}</dd>
- <dt>项目名称</dt><dd>${p.name || '—'}</dd>
- <dt>在管服务</dt><dd>${p.serviceScope || '—'}</dd>
- </dl>
- </div>
- </div>
- <div class="card">
- <div class="card-head">续标状态</div>
- <div class="card-body">
- <p style="font-size:13px;color:var(--text2)">${p.listStatus === 'completed' ? '本项目续标已完成,可在历史记录中查看。' : days <= 90 ? '该项目即将进入续标关键期,建议提前启动客户回访与现场评估。' : '系统持续监控合同到期日与招标平台,到期前自动预警并创建续标任务。'}</p>
- ${p.listStatus !== 'completed' ? `<button type="button" class="btn btn-primary" style="margin-top:12px" onclick="toast('续标流程将在合同到期前90天自动启动')">提前启动续标</button>` : ''}
- </div>
- </div>`;
- }
- function buildRenewalTimeline() {
- const items = [
- { text: '合同到期预警触发', done: true },
- { text: '续标准备(回访、踏勘)', done: appState.renewalTasks.find(x => x.id === 'r2').done },
- { text: '招标公告监控', done: getRenewalTender()?.status === 'matched' },
- { text: '标书拆解与生成', done: appState.bidExported },
- { text: '投标登记', done: appState.renewalTasks.find(x => x.id === 'r6').done },
- { text: '开标结果抓取', done: appState.bidSubmissions.some(s => s.status === 'result_fetched' || s.status === 'pushed') },
- { text: '中标推送完成', done: appState.renewalTasks.find(x => x.id === 'r7').done },
- ];
- return items;
- }
- function onRenewalChecklistClick(id) {
- const t = appState.renewalTasks.find(x => x.id === id);
- if (t?.done) return;
- if (id === 'r3') simulateTenderMatch();
- else if (id === 'r4') fetchTenderFromRenewal();
- else if (id === 'r5') navigate('bid-task');
- else if (id === 'r6') { navigate('bid-result'); setResultTab('register'); prefillRegisterForm(); }
- else if (id === 'r7') { navigate('bid-result'); setResultTab('win'); }
- }
- function completeRenewalPrep() {
- appState.renewalTasks.find(x => x.id === 'r1').done = true;
- appState.renewalTasks.find(x => x.id === 'r2').done = true;
- toast('续标准备已完成');
- renderRenewalHub();
- renderProject();
- }
- function fetchTenderFromRenewal() {
- fetchTenderDoc('t-renewal', true);
- }
- function scrollRenewalTender() {
- document.getElementById('renewal-tender-card')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- function renderProject() {
- const p = renewalDemo;
- const days = daysUntil(p.contractEnd);
- document.getElementById('project-title').textContent = p.displayName;
- document.getElementById('project-sub').textContent = `${p.name} · ${p.kingdeeCode}`;
- document.getElementById('project-banner').innerHTML = `
- <div class="banner-renewal urgent">
- <div>
- <strong>续标预警</strong> · 合同起止 ${p.contractPeriod},距本期到期 <strong>${days}</strong> 天
- <div style="font-size:12px;color:var(--text2);margin-top:6px">服务期限 ${p.servicePeriod} · 请进入项目续标跟进全流程。</div>
- </div>
- <button type="button" class="btn btn-primary" onclick="startRenewal()">进入项目续标</button>
- </div>
- `;
- renderProjectFields(p);
- document.getElementById('project-bids').innerHTML = (p.historyBids || []).map(b =>
- `<dt>${b.year}年</dt><dd>${b.result} · ${formatYuan(b.amount)}</dd>`
- ).join('') || '<dt>—</dt><dd>暂无</dd>';
- document.getElementById('btn-start-renewal').textContent = '进入项目续标';
- document.getElementById('btn-start-renewal').onclick = startRenewal;
- }
- function onChecklistClick() { startRenewal(); }
- function renderMarket() {
- const t = getRenewalTender();
- const matched = t && t.status === 'matched';
- document.getElementById('tender-monitor-status').textContent = matched
- ? `续标已匹配 · 投标 ${bidPipeline.length} · 储备 ${reserveProjects.length}`
- : `待跟进 ${tenderPool.filter(t => !t.convertedTo).length} · 投标 ${bidPipeline.length} · 储备 ${reserveProjects.length}`;
- document.getElementById('market-pool-badge').textContent = matched ? '续标已匹配' : '今日 +3';
- document.getElementById('market-pool-badge').className = matched ? 'tag tag-danger' : 'tag';
- const monitoring = appState.bidSubmissions.filter(s => s.status === 'monitoring').length;
- const badge = document.getElementById('market-result-badge');
- if (badge) badge.textContent = monitoring ? `监控 ${monitoring}` : (appState.bidSubmissions.some(s => s.status === 'result_fetched') ? '待登记' : '监控 0');
- }
- function renderTenderPool() {
- syncBidPipelineItems();
- const search = (document.getElementById('pool-search')?.value || '').toLowerCase();
- const source = document.getElementById('pool-source')?.value || '';
- const district = document.getElementById('pool-district')?.value || '';
- const filter = appState.poolFilter;
- let list = tenderPool.filter(t => {
- if (filter !== 'converted' && t.convertedTo) return false;
- if (filter === 'converted' && !t.convertedTo) return false;
- if (filter === 'renewal' && t.type !== 'renewal') return false;
- if (filter === 'high' && t.matchScore < 80) return false;
- if (filter === 'today' && t.publishDate !== '2026-06-08' && t.publishDate !== '2026-06-07') return false;
- if (filter === 'new' && t.status === 'read') return false;
- if (source && t.source !== source) return false;
- if (district && t.district !== district) return false;
- if (search && !t.title.toLowerCase().includes(search) && !t.keywords.some(k => k.includes(search))) return false;
- return true;
- });
- const poolActive = tenderPool.filter(t => !t.convertedTo).length;
- const matched = tenderPool.filter(t => t.type === 'renewal' && t.status === 'matched').length;
- const todayNew = tenderPool.filter(t => !t.convertedTo && t.publishDate >= '2026-06-07').length;
- const high = tenderPool.filter(t => !t.convertedTo && t.matchScore >= 80).length;
- document.getElementById('pool-stats').innerHTML = `
- <div class="pool-stat ${filter === 'all' ? 'highlight' : ''}" onclick="setPoolFilter('all')"><span>待跟进</span><b>${poolActive}</b></div>
- <div class="pool-stat" onclick="navigate('tender-bids')"><span>投标项目</span><b>${bidPipeline.length}</b></div>
- <div class="pool-stat" onclick="navigate('tender-reserve')"><span>储备项目</span><b>${reserveProjects.length}</b></div>
- <div class="pool-stat ${matched ? 'highlight' : ''}" onclick="setPoolFilter('renewal')"><span>续标监控</span><b>${tenderPool.filter(t => t.type === 'renewal').length}</b></div>
- `;
- const linkBids = document.getElementById('pool-link-bids');
- const linkReserve = document.getElementById('pool-link-reserve');
- if (linkBids) linkBids.textContent = bidPipeline.length ? `(${bidPipeline.length})` : '';
- if (linkReserve) linkReserve.textContent = reserveProjects.length ? `(${reserveProjects.length})` : '';
- const chips = [
- ['all', '待跟进'], ['renewal', '续标监控'], ['high', '高优'], ['today', '近2日'], ['new', '未读'],
- ];
- document.getElementById('pool-chips').innerHTML = chips.map(([k, l]) =>
- `<button type="button" class="chip ${filter === k ? 'active' : ''}" onclick="setPoolFilter('${k}')">${l}</button>`
- ).join('');
- document.getElementById('tender-list').innerHTML = list.length ? list.map(t => {
- const typeTag = t.type === 'renewal'
- ? `<span class="tag tag-renewal">${t.status === 'matched' ? '续标已匹配' : '续标监控'}</span>`
- : `<span class="tag tag-new">新标</span>`;
- const scoreCls = t.matchScore >= 80 ? 'high' : '';
- const daysLeft = Math.ceil((new Date(t.deadline) - DEMO_TODAY) / 86400000);
- return `<div class="tender-row ${appState.selectedTenderId === t.id ? 'selected' : ''}" onclick="openTenderDrawer('${t.id}')">
- <div>
- <div class="tender-title">${t.title}</div>
- <div class="tender-meta">
- <span>${t.source}</span><span>${t.district}</span>
- <span>发布 ${t.publishDate}</span><span>截止 ${t.deadline}${daysLeft > 0 ? ' · 剩' + daysLeft + '天' : ''}</span>
- <span>预算 ${formatMoney(t.budget)}</span>
- ${typeTag}
- <span class="score-badge ${scoreCls}">匹配 ${t.matchScore}</span>
- </div>
- </div>
- <div class="tender-actions" onclick="event.stopPropagation()">
- <button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="openTenderDrawer('${t.id}')">详情</button>
- <button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="convertToBid('${t.id}')">转投标</button>
- <button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="convertToReserve('${t.id}')">转储备</button>
- </div>
- </div>`;
- }).join('') : '<div style="padding:32px;text-align:center;color:var(--text3)">暂无待跟进信息 · 已转入的项目请查看投标/储备列表</div>';
- }
- function buildBidPipelineItem(t) {
- const linked = t.linkedProjectId === 'renewal-demo' ? renewalDemo.displayName : null;
- return {
- id: 'bp-' + t.id,
- tenderId: t.id,
- title: t.title,
- district: t.district,
- source: t.source,
- budget: t.budget,
- matchScore: t.matchScore,
- deadline: t.deadline,
- convertedAt: '2026-06-08',
- linkedProject: linked,
- type: t.type,
- status: 'preparing',
- bidProgress: 0,
- };
- }
- function computeBidPipelineStatus(tenderId) {
- if (tenderId === 't-renewal') {
- if (appState.bidSubmissions.some(s => s.projectId === renewalDemo.id && s.status === 'monitoring')) return 'monitoring';
- if (appState.bidSubmissions.some(s => s.projectId === renewalDemo.id && (s.status === 'result_fetched' || s.status === 'pushed'))) return 'submitted';
- if (appState.bidExported) return 'ready';
- if (appState.bidTaskFromTender) return 'preparing';
- }
- const item = bidPipeline.find(x => x.tenderId === tenderId);
- return item?.status || 'preparing';
- }
- function syncBidPipelineItems() {
- bidPipeline.forEach(item => {
- if (item.tenderId === 't-renewal') {
- item.bidProgress = Math.round((appState.confirmedBlocks.size / bidBlocks.length) * 100);
- item.linkedProject = renewalDemo.displayName;
- }
- item.status = computeBidPipelineStatus(item.tenderId);
- });
- }
- function ensureBidPipelineItem(tenderId) {
- const t = tenderPool.find(x => x.id === tenderId);
- if (!t) return null;
- let item = bidPipeline.find(x => x.tenderId === tenderId);
- if (!item) {
- item = buildBidPipelineItem(t);
- bidPipeline.unshift(item);
- }
- t.convertedTo = 'bid';
- syncBidPipelineItems();
- return item;
- }
- function convertToBid(tenderId, goList) {
- const t = tenderPool.find(x => x.id === tenderId);
- if (!t) return;
- if (t.convertedTo === 'bid') {
- toast('已在投标项目列表中');
- if (goList) navigate('tender-bids');
- return;
- }
- reserveProjects = reserveProjects.filter(r => r.tenderId !== tenderId);
- ensureBidPipelineItem(tenderId);
- closeTenderDrawer();
- toast('已转入投标项目 · ' + t.title.slice(0, 20) + '…');
- renderTenderPool();
- renderTenderBidList();
- renderTenderReserveList();
- renderMarket();
- if (goList) navigate('tender-bids');
- }
- function convertToReserve(tenderId, goList) {
- const t = tenderPool.find(x => x.id === tenderId);
- if (!t) return;
- if (t.convertedTo === 'bid') { toast('该项目已转入投标,请先在投标列表中处理'); return; }
- if (t.convertedTo === 'reserve' || reserveProjects.some(r => r.tenderId === tenderId)) {
- toast('已在储备项目列表中');
- if (goList) navigate('tender-reserve');
- return;
- }
- t.convertedTo = 'reserve';
- reserveProjects.unshift({
- id: 'rs-' + Date.now(),
- tenderId: t.id,
- title: t.title,
- district: t.district,
- source: t.source,
- budget: t.budget,
- matchScore: t.matchScore,
- convertedAt: '2026-06-08',
- deadline: t.deadline,
- status: t.matchScore >= 80 ? 'evaluating' : 'watching',
- reason: t.matchScore >= 80 ? '业态匹配,待市场部评估人力与报价区间' : '匹配度一般,纳入观察清单',
- owner: '市场部',
- });
- closeTenderDrawer();
- toast('已转入储备项目');
- renderTenderPool();
- renderTenderReserveList();
- renderMarket();
- if (goList) navigate('tender-reserve');
- }
- function reserveToBid(reserveId) {
- const r = reserveProjects.find(x => x.id === reserveId);
- if (!r) return;
- reserveProjects = reserveProjects.filter(x => x.id !== reserveId);
- convertToBid(r.tenderId, false);
- toast('储备项目已转投标');
- renderTenderReserveList();
- renderTenderBidList();
- }
- function removeFromReserve(reserveId) {
- const r = reserveProjects.find(x => x.id === reserveId);
- if (!r) return;
- const t = tenderPool.find(x => x.id === r.tenderId);
- if (t) t.convertedTo = null;
- reserveProjects = reserveProjects.filter(x => x.id !== reserveId);
- toast('已移出储备,招标信息恢复待跟进');
- renderTenderReserveList();
- renderTenderPool();
- renderMarket();
- }
- function bidStatusLabel(status) {
- return {
- preparing: ['标书编制中', 'tag-warn'],
- ready: ['待投标', 'tag-warn'],
- monitoring: ['开标监控', 'tag-danger'],
- submitted: ['已投标', 'tag'],
- }[status] || ['跟进中', 'tag'];
- }
- function reserveStatusLabel(status) {
- return {
- watching: ['观察中', 'tag'],
- evaluating: ['评估中', 'tag-warn'],
- ready: ['待转投标', 'tag-bid'],
- }[status] || ['观察中', 'tag'];
- }
- function setBidPipelineFilter(f) {
- appState.bidPipelineFilter = f;
- renderTenderBidList();
- }
- function setReserveFilter(f) {
- appState.reserveFilter = f;
- renderTenderReserveList();
- }
- function renderTenderBidList() {
- syncBidPipelineItems();
- const search = (document.getElementById('bid-pipeline-search')?.value || '').toLowerCase();
- const filter = appState.bidPipelineFilter;
- let list = [...bidPipeline];
- if (search) list = list.filter(x => x.title.toLowerCase().includes(search));
- if (filter === 'renewal') list = list.filter(x => x.type === 'renewal');
- else if (filter === 'preparing') list = list.filter(x => x.status === 'preparing' || x.status === 'ready');
- else if (filter === 'monitoring') list = list.filter(x => x.status === 'monitoring' || x.status === 'submitted');
- const preparing = bidPipeline.filter(x => x.status === 'preparing' || x.status === 'ready').length;
- const monitoring = bidPipeline.filter(x => x.status === 'monitoring' || x.status === 'submitted').length;
- const renewal = bidPipeline.filter(x => x.type === 'renewal').length;
- document.getElementById('bid-pipeline-stats').innerHTML = `
- <div class="pool-stat ${filter === 'all' ? 'highlight' : ''}" onclick="setBidPipelineFilter('all')"><span>全部</span><b>${bidPipeline.length}</b></div>
- <div class="pool-stat ${filter === 'preparing' ? 'highlight' : ''}" onclick="setBidPipelineFilter('preparing')"><span>标书阶段</span><b>${preparing}</b></div>
- <div class="pool-stat ${filter === 'monitoring' ? 'highlight' : ''}" onclick="setBidPipelineFilter('monitoring')"><span>投标/开标</span><b>${monitoring}</b></div>
- <div class="pool-stat ${filter === 'renewal' ? 'highlight' : ''}" onclick="setBidPipelineFilter('renewal')"><span>续标投标</span><b>${renewal}</b></div>
- `;
- document.getElementById('bid-pipeline-chips').innerHTML = [
- ['all', '全部'], ['renewal', '续标'], ['preparing', '标书编制'], ['monitoring', '投标开标'],
- ].map(([k, l]) => `<button type="button" class="chip ${filter === k ? 'active' : ''}" onclick="setBidPipelineFilter('${k}')">${l}</button>`).join('');
- if (!list.length) {
- document.getElementById('bid-pipeline-list').innerHTML = `<div class="empty-shell" style="border:none;padding:32px">
- <strong>暂无投标项目</strong><p>在招标监测中将信息「转投标」后在此跟进</p>
- <button type="button" class="btn btn-primary" style="margin-top:12px" onclick="navigate('tender-pool')">去招标监测 →</button></div>`;
- return;
- }
- document.getElementById('bid-pipeline-list').innerHTML = list.map(item => {
- const [label, tag] = bidStatusLabel(item.status);
- const daysLeft = Math.ceil((new Date(item.deadline) - DEMO_TODAY) / 86400000);
- return `<div class="pipeline-row">
- <div>
- <div class="pipeline-row-title">${item.title}</div>
- <div class="pipeline-row-meta">
- <span>${item.district}</span>
- <span>预算 ${formatMoney(item.budget)}</span>
- <span>截止 ${item.deadline}${daysLeft > 0 ? ' · 剩' + daysLeft + '天' : ''}</span>
- <span>转入 ${item.convertedAt}</span>
- ${item.linkedProject ? `<span>关联 ${item.linkedProject}</span>` : ''}
- ${item.bidProgress != null && item.status === 'preparing' ? `<span>标书进度 ${item.bidProgress}%</span>` : ''}
- </div>
- </div>
- <div class="pipeline-row-actions">
- <span class="tag ${tag}">${label}</span>
- ${item.type === 'renewal' ? '<span class="tag tag-renewal">续标</span>' : ''}
- ${item.status === 'preparing' || item.status === 'ready' ? `<button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="openBidFromPipeline('${item.tenderId}')">标书编制 →</button>` : ''}
- ${item.status === 'monitoring' || item.status === 'submitted' ? `<button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="navigate('bid-result');setResultTab('monitor')">开标监控 →</button>` : ''}
- <button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="openTenderDrawer('${item.tenderId}')">原公告</button>
- </div>
- </div>`;
- }).join('');
- }
- function openBidFromPipeline(tenderId) {
- if (tenderId === 't-renewal') {
- navigate('bid-task');
- return;
- }
- toast('标书编制功能演示 · 续标项目已完整接入');
- navigate('bid-workbench');
- }
- function renderTenderReserveList() {
- const search = (document.getElementById('reserve-search')?.value || '').toLowerCase();
- const district = document.getElementById('reserve-district')?.value || '';
- const filter = appState.reserveFilter;
- let list = [...reserveProjects];
- if (search) list = list.filter(x => (x.title + x.district + x.reason).toLowerCase().includes(search));
- if (district) list = list.filter(x => x.district === district);
- if (filter === 'evaluating') list = list.filter(x => x.status === 'evaluating');
- else if (filter === 'watching') list = list.filter(x => x.status === 'watching');
- else if (filter === 'high') list = list.filter(x => x.matchScore >= 80);
- const evaluating = reserveProjects.filter(x => x.status === 'evaluating').length;
- const watching = reserveProjects.filter(x => x.status === 'watching').length;
- const high = reserveProjects.filter(x => x.matchScore >= 80).length;
- document.getElementById('reserve-stats').innerHTML = `
- <div class="pool-stat ${filter === 'all' ? 'highlight' : ''}" onclick="setReserveFilter('all')"><span>全部</span><b>${reserveProjects.length}</b></div>
- <div class="pool-stat ${filter === 'evaluating' ? 'highlight' : ''}" onclick="setReserveFilter('evaluating')"><span>评估中</span><b>${evaluating}</b></div>
- <div class="pool-stat ${filter === 'watching' ? 'highlight' : ''}" onclick="setReserveFilter('watching')"><span>观察中</span><b>${watching}</b></div>
- <div class="pool-stat ${filter === 'high' ? 'highlight' : ''}" onclick="setReserveFilter('high')"><span>高匹配 ≥80</span><b>${high}</b></div>
- `;
- document.getElementById('reserve-chips').innerHTML = [
- ['all', '全部'], ['evaluating', '评估中'], ['watching', '观察中'], ['high', '高匹配'],
- ].map(([k, l]) => `<button type="button" class="chip ${filter === k ? 'active' : ''}" onclick="setReserveFilter('${k}')">${l}</button>`).join('');
- if (!list.length) {
- document.getElementById('reserve-list').innerHTML = `<div class="empty-shell" style="border:none;padding:32px">
- <strong>暂无储备项目</strong><p>在招标监测中将信息「转储备」后在此评估跟进</p>
- <button type="button" class="btn btn-primary" style="margin-top:12px" onclick="navigate('tender-pool')">去招标监测 →</button></div>`;
- return;
- }
- document.getElementById('reserve-list').innerHTML = list.map(r => {
- const [label, tag] = reserveStatusLabel(r.status);
- const daysLeft = Math.ceil((new Date(r.deadline) - DEMO_TODAY) / 86400000);
- return `<div class="pipeline-row">
- <div>
- <div class="pipeline-row-title">${r.title}</div>
- <div class="pipeline-row-meta">
- <span>${r.district}</span>
- <span>预算 ${formatMoney(r.budget)}</span>
- <span>匹配 ${r.matchScore}</span>
- <span>截止 ${r.deadline}${daysLeft > 0 ? ' · 剩' + daysLeft + '天' : ''}</span>
- <span>转入 ${r.convertedAt}</span>
- <span>${r.owner}</span>
- </div>
- <div style="font-size:12px;color:var(--text2);margin-top:6px">${r.reason}</div>
- </div>
- <div class="pipeline-row-actions">
- <span class="tag tag-reserve">${label}</span>
- <button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="reserveToBid('${r.id}')">转投标 →</button>
- <button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="openTenderDrawer('${r.tenderId}')">原公告</button>
- <button type="button" class="btn btn-ghost" style="font-size:12px;padding:4px 10px" onclick="removeFromReserve('${r.id}')">移出</button>
- </div>
- </div>`;
- }).join('');
- }
- function setPoolFilter(f) {
- appState.poolFilter = f;
- renderTenderPool();
- }
- function openTenderDrawer(id) {
- const t = tenderPool.find(x => x.id === id);
- if (!t) return;
- appState.selectedTenderId = id;
- document.getElementById('drawer-title').textContent = t.title;
- document.getElementById('drawer-sub').textContent = `${t.source} · ${t.district} · 发布 ${t.publishDate}${t.tenderNo ? ' · ' + t.tenderNo : ''}`;
- const linked = t.linkedProjectId === 'renewal-demo' ? renewalDemo.displayName : '—';
- const sourceUrl = t.sourceUrl || '';
- document.getElementById('drawer-body').innerHTML = `
- <div class="drawer-section">
- <h4>原文链接</h4>
- ${sourceUrl ? `
- <div class="source-link-box">
- <span style="font-size:16px;flex-shrink:0">🔗</span>
- <div>
- <div style="font-size:11px;color:var(--text3);margin-bottom:4px">${t.source} · 抓取源页面</div>
- <a href="${sourceUrl}" target="_blank" rel="noopener noreferrer">${sourceUrl}</a>
- </div>
- </div>` : '<p style="font-size:13px;color:var(--text3)">暂无原文链接</p>'}
- </div>
- <div class="drawer-section">
- <h4>基本信息</h4>
- <dl class="info">
- <dt>信息来源</dt><dd>${t.source}</dd>
- <dt>招标编号</dt><dd>${t.tenderNo || '—'}</dd>
- <dt>预算金额</dt><dd>${formatMoney(t.budget)}/年</dd>
- <dt>投标截止</dt><dd>${t.deadline}</dd>
- <dt>匹配得分</dt><dd><span class="score-badge ${t.matchScore >= 80 ? 'high' : ''}">${t.matchScore} 分</span></dd>
- <dt>关联项目</dt><dd>${linked}</dd>
- <dt>关键词</dt><dd>${t.keywords.join('、')}</dd>
- </dl>
- </div>
- <div class="drawer-section">
- <h4>公告摘要</h4>
- <p style="font-size:13px;color:var(--text2)">${t.content}</p>
- </div>
- <div class="drawer-section">
- <h4>智能评估</h4>
- <p style="font-size:13px">${t.type === 'renewal' ? '🟢 与在管续标项目高度匹配,建议优先跟进' : t.matchScore >= 80 ? '🟡 业态匹配,可纳入储备' : '⚪ 匹配度一般,可观察'}</p>
- </div>
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:20px">
- ${sourceUrl ? `<a class="btn btn-primary" href="${sourceUrl}" target="_blank" rel="noopener noreferrer">打开原公告链接 ↗</a>` : ''}
- ${t.type === 'renewal' && !t.convertedTo ? `<button type="button" class="btn btn-primary" onclick="fetchTenderDoc('${t.id}')">获取标书并拆解</button>` : ''}
- ${!t.convertedTo ? `<button type="button" class="btn btn-primary" onclick="convertToBid('${t.id}', true)">转投标</button>` : ''}
- ${t.convertedTo === 'bid' ? `<button type="button" class="btn btn-primary" onclick="navigate('tender-bids')">查看投标项目 →</button>` : ''}
- ${!t.convertedTo ? `<button type="button" class="btn" onclick="convertToReserve('${t.id}', true)">转储备项目</button>` : ''}
- ${t.convertedTo === 'reserve' ? `<button type="button" class="btn" onclick="navigate('tender-reserve')">查看储备项目 →</button>` : ''}
- <button type="button" class="btn btn-ghost" onclick="closeTenderDrawer()">关闭</button>
- </div>
- `;
- document.getElementById('tender-drawer-mask').classList.remove('hidden');
- renderTenderPool();
- }
- function closeTenderDrawer() {
- document.getElementById('tender-drawer-mask').classList.add('hidden');
- appState.selectedTenderId = null;
- renderTenderPool();
- }
- function fetchTenderDoc(id, returnToRenewal) {
- const t = tenderPool.find(x => x.id === id);
- if (!t) return;
- t.status = 'matched';
- ensureBidPipelineItem(id);
- appState.renewalTasks.find(x => x.id === 'r3').done = true;
- appState.renewalTasks.find(x => x.id === 'r4').done = true;
- appState.bidTaskFromTender = true;
- bidBlocks.filter(b => b.type === 'auto').forEach(b => appState.confirmedBlocks.add(b.id));
- appState.activeBlockId = bidBlocks.find(b => !appState.confirmedBlocks.has(b.id))?.id || 'b3';
- closeTenderDrawer();
- toast('招标文件已获取' + (returnToRenewal ? ' · 已返回项目续标' : '') + ' · 请确认剩余 ' + (bidBlocks.length - appState.confirmedBlocks.size) + ' 块');
- renderDashboard();
- renderProject();
- renderMarket();
- renderTenderBidList();
- setTimeout(() => navigate(returnToRenewal ? 'renewal-detail' : 'bid-task'), 400);
- }
- function renderBidWorkbench() {
- const days = daysUntil(renewalDemo.contractEnd);
- const progress = Math.round((appState.confirmedBlocks.size / bidBlocks.length) * 100);
- const autoDone = bidBlocks.filter(b => b.type === 'auto').length;
- const pending = bidBlocks.length - appState.confirmedBlocks.size;
- document.getElementById('bid-task-list').innerHTML = `
- <div class="bid-task-row">
- <div>
- <strong>${renewalDemo.displayName} · 续标投标</strong>
- <div style="font-size:12px;color:var(--text3);margin-top:4px">
- 合同剩 ${days} 天 · ${appState.bidTaskFromTender ? '招标文件已关联 · 拆解 ' + bidBlocks.length + ' 块' : '待从招标监测获取'}
- </div>
- <div style="font-size:11px;color:var(--text3);margin-top:4px">
- 🟢 自动 ${autoDone} 块 · 待确认 ${pending} 块 · 数据映射 ${bidBlocks.flatMap(b => b.mappings || []).filter(m => m.status === 'warn').length} 项
- </div>
- <div class="progress-bar" style="max-width:280px;margin-top:8px"><i style="width:${progress}%"></i></div>
- </div>
- <div>
- <span class="tag tag-warn">${progress === 100 ? (appState.bidExported ? '已导出' : '待导出') : progress > 0 ? '确认中 ' + progress + '%' : appState.bidTaskFromTender ? '拆解完成' : '待拆解'}</span>
- <button type="button" class="btn btn-primary" style="margin-left:8px" onclick="navigate('bid-task')">继续 →</button>
- </div>
- </div>
- `;
- }
- function getBlockDraft(id) {
- if (appState.blockDrafts[id] != null) return appState.blockDrafts[id];
- const b = bidBlocks.find(x => x.id === id);
- return b?.draft || '';
- }
- function updateBlockDraft(id, val) {
- appState.blockDrafts[id] = val;
- }
- function escapeHtml(s) {
- return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
- }
- function getDraftStats(text) {
- const chars = text.length;
- const lines = text ? text.split(/\n/).length : 0;
- return { chars, lines };
- }
- function regenerateBlockDraft(id) {
- const b = bidBlocks.find(x => x.id === id);
- if (!b) return;
- appState.blockDrafts[id] = b.draft;
- toast('已恢复 AI 生成草稿');
- renderBidTask();
- setTimeout(() => document.getElementById('block-draft-editor')?.focus(), 50);
- }
- function renderBlockRefPanel(block) {
- if (!block) return '<p style="color:var(--text3);font-size:12px">请选择拆解块</p>';
- const reqHtml = (block.requirements || []).map(r => `<li class="matched">${r}</li>`).join('');
- const mapHtml = (block.mappings || []).map(m =>
- `<tr><td>${m.field}</td><td>${m.source}</td><td>${m.value}</td><td class="${m.status === 'ok' ? 'mapping-ok' : 'mapping-warn'}">${m.status === 'ok' ? '✓' : '⚠'}</td></tr>`
- ).join('');
- const scoreHtml = block.scores
- ? `<div class="score-grid">${block.scores.map(s => `<div class="score-item"><b>${s.pts}</b>${s.label}</div>`).join('')}</div>`
- : '';
- return `
- <div class="bid-ref-scroll">
- <div style="font-size:11px;color:var(--text3);margin-bottom:6px">提取要点 · ${block.docRef}</div>
- <ul class="req-list">${reqHtml}</ul>
- ${scoreHtml}
- <div style="margin-top:10px;font-size:11px;font-weight:600;color:var(--text2)">数据映射</div>
- <table class="mapping-table compact">
- <thead><tr><th>字段</th><th>来源</th><th>值</th><th></th></tr></thead>
- <tbody>${mapHtml}</tbody>
- </table>
- ${block.historyNote ? `<div class="history-compare" style="margin-top:10px;font-size:11px">📋 ${block.historyNote}</div>` : ''}
- </div>`;
- }
- function renderBlockEditorPanel(block) {
- if (!block) {
- return `<div class="bid-editor-toolbar"><div class="bid-editor-toolbar-left"><h3>草稿编辑</h3></div></div>
- <div class="bid-editor-body"><div class="bid-draft-readonly" style="color:var(--text3)">请从上方选择拆解块后开始编辑</div></div>`;
- }
- const confirmed = appState.confirmedBlocks.has(block.id);
- const draft = getBlockDraft(block.id);
- const editable = block.type === 'manual' || block.type === 'confirm';
- const stats = getDraftStats(draft);
- const [typeLabel, typeCls] = fillTypeLabel(block.type);
- return `
- <div class="bid-editor-toolbar">
- <div class="bid-editor-toolbar-left">
- <h3>${block.title}</h3>
- <span class="${typeCls}" style="font-size:12px">${typeLabel}</span>
- ${confirmed ? '<span class="tag">已确认</span>' : '<span class="tag tag-warn">待确认 · 请在下方编辑后确认</span>'}
- </div>
- <div class="bid-editor-toolbar-right">
- ${editable && !confirmed ? `
- <button type="button" class="btn" onclick="regenerateBlockDraft('${block.id}')">恢复 AI 草稿</button>
- ${block.type === 'manual' ? `<button type="button" class="btn" onclick="toast('AI 已基于历史标书重新生成草稿');regenerateBlockDraft('${block.id}')">重新生成</button>` : ''}
- <button type="button" class="btn btn-primary" onclick="confirmBlock('${block.id}')">确认本块</button>
- ` : ''}
- ${confirmed ? `<button type="button" class="btn" onclick="selectBlock('${block.id}');toast('已确认块如需修改请重新打开')">查看</button>` : ''}
- </div>
- </div>
- <div class="bid-editor-body">
- ${editable && !confirmed
- ? `<textarea class="bid-draft-editor" id="block-draft-editor" placeholder="在此编辑本块投标内容…支持多段落,确认后将写入投标文件预览。" oninput="updateBlockDraft('${block.id}', this.value);updateDraftStats()">${escapeHtml(draft)}</textarea>`
- : `<div class="bid-draft-readonly">${escapeHtml(draft)}</div>`}
- </div>
- <div class="bid-editor-foot">
- <span>${editable && !confirmed ? '提示:编辑区约占页面 1/3 以上,可直接修改 AI 草稿内容' : '🟢 本块由系统自动填充,无需人工编辑'}</span>
- <span id="draft-stats">${stats.chars} 字 · ${stats.lines} 行</span>
- </div>`;
- }
- function updateDraftStats() {
- const el = document.getElementById('block-draft-editor');
- const statEl = document.getElementById('draft-stats');
- if (!el || !statEl) return;
- const stats = getDraftStats(el.value);
- statEl.textContent = stats.chars + ' 字 · ' + stats.lines + ' 行';
- const preview = document.getElementById('bid-preview-right');
- if (preview && appState.activeBlockId) {
- preview.innerHTML = buildPreview();
- }
- }
- function renderBlockDetail(block) {
- return renderBlockRefPanel(block);
- }
- function renderBidTask() {
- const p = renewalDemo;
- const progress = Math.round((appState.confirmedBlocks.size / bidBlocks.length) * 100);
- const autoCount = bidBlocks.filter(b => b.type === 'auto').length;
- const confirmCount = bidBlocks.filter(b => b.type === 'confirm').length;
- const manualCount = bidBlocks.filter(b => b.type === 'manual').length;
- const autoDone = bidBlocks.filter(b => b.type === 'auto' && appState.confirmedBlocks.has(b.id)).length;
- const warnMappings = bidBlocks.flatMap(b => b.mappings || []).filter(m => m.status === 'warn').length;
- document.getElementById('bid-task-title').textContent = `${p.displayName} · 续标投标`;
- document.getElementById('bid-progress-bar').style.width = progress + '%';
- document.getElementById('btn-export-word').disabled = progress < 100;
- document.getElementById('btn-register-bid').disabled = !appState.bidExported;
- const step = progress === 100 ? 4 : progress > 0 ? 3 : appState.bidTaskFromTender ? 2 : 1;
- document.getElementById('bid-task-steps').innerHTML = `
- <span class="step ${step >= 1 ? 'done' : ''}">1 上传${appState.bidTaskFromTender ? ' ✓' : ''}</span>
- <span class="step ${step === 2 ? 'active' : step > 2 ? 'done' : ''}">2 智能拆解</span>
- <span class="step ${step === 3 ? 'active' : step > 3 ? 'done' : ''}">3 分步确认 (${appState.confirmedBlocks.size}/${bidBlocks.length})</span>
- <span class="step ${step === 4 ? 'active' : ''}">4 生成导出</span>
- `;
- document.getElementById('bid-summary-bar').innerHTML = `
- <div class="bid-summary-item"><span>拆解进度</span><b>${progress}%</b></div>
- <div class="bid-summary-item"><span>🟢 自动填充</span><b>${autoDone}/${autoCount}</b></div>
- <div class="bid-summary-item"><span>🟡 需确认</span><b>${bidBlocks.filter(b => b.type === 'confirm' && appState.confirmedBlocks.has(b.id)).length}/${confirmCount}</b></div>
- <div class="bid-summary-item"><span>⚠ 待处理映射</span><b>${warnMappings}</b></div>
- `;
- const activeBlock = bidBlocks.find(b => b.id === appState.activeBlockId);
- document.getElementById('bid-doc-left').innerHTML = `
- <div style="font-size:11px;color:var(--text3);margin-bottom:8px">招标文件 · 点击段落定位对应拆解块</div>
- ${tenderDocLines.map((line, i) =>
- `<div class="doc-line ${activeBlock && activeBlock.docLine === i ? 'active highlight' : ''}" onclick="highlightDocLine(${i})">${line}</div>`
- ).join('')}
- <div style="margin-top:12px;padding:10px;background:#f0fdf4;border-radius:6px;font-size:11px;color:#059669">
- 已关联 ${bidBlocks.length} 个拆解块 · ${appState.bidTaskFromTender ? '招标文件已解析' : '待上传解析'}
- </div>`;
- document.getElementById('bid-blocks-mid').innerHTML = `
- <div style="font-size:11px;color:var(--text3);margin-bottom:8px">点击切换块 · 在下方编辑区修改草稿</div>
- <div class="bid-blocks-compact">
- ${bidBlocks.map(b => {
- const [label, cls] = fillTypeLabel(b.type);
- const confirmed = appState.confirmedBlocks.has(b.id);
- const warnCount = (b.mappings || []).filter(m => m.status === 'warn').length;
- return `<div class="block-item compact ${b.id === appState.activeBlockId ? 'active' : ''} ${confirmed ? 'confirmed' : ''}" onclick="selectBlock('${b.id}')">
- <strong>${b.title}</strong>
- <div class="block-meta"><span class="${cls}">${label}</span>${warnCount ? ` · <span class="mapping-warn">${warnCount}项待核</span>` : ''}</div>
- </div>`;
- }).join('')}
- </div>
- ${renderBlockRefPanel(activeBlock)}
- `;
- const editorZone = document.getElementById('bid-editor-zone');
- const editable = activeBlock && (activeBlock.type === 'manual' || activeBlock.type === 'confirm') && !appState.confirmedBlocks.has(activeBlock.id);
- editorZone.className = 'bid-editor-zone' + (editable ? '' : ' readonly');
- editorZone.innerHTML = renderBlockEditorPanel(activeBlock);
- document.getElementById('bid-preview-right').innerHTML = buildPreview();
- if (editable) {
- setTimeout(() => {
- const ta = document.getElementById('block-draft-editor');
- if (ta) {
- ta.focus();
- ta.setSelectionRange(ta.value.length, ta.value.length);
- }
- }, 30);
- }
- }
- function buildPreview() {
- const p = renewalDemo;
- let html = `<h4>上海申勤物业发展有限公司</h4>
- <p style="font-size:11px;color:var(--text3)">投标文件(技术标 · 实时预览)</p>
- <p style="font-size:11px;color:var(--text3);margin-bottom:12px">${p.displayName} · ${p.name}</p>`;
- bidBlocks.forEach(b => {
- const isActive = b.id === appState.activeBlockId;
- const showInPreview = appState.confirmedBlocks.has(b.id) || b.type === 'auto' || isActive;
- if (!showInPreview) return;
- const draft = getBlockDraft(b.id);
- const [label] = fillTypeLabel(b.type);
- const activeMark = isActive && !appState.confirmedBlocks.has(b.id) ? ' · 编辑中' : '';
- html += `<h4>${b.title} <span style="font-size:11px;font-weight:400;color:var(--text3)">${label}${activeMark}</span></h4>`;
- html += `<p style="white-space:pre-wrap">${escapeHtml(isActive ? draft : draft.slice(0, 200))}${!isActive && draft.length > 200 ? '…' : ''}</p>`;
- });
- const pending = bidBlocks.filter(b => !appState.confirmedBlocks.has(b.id) && b.type !== 'auto');
- if (pending.length) {
- html += `<div style="margin-top:16px;padding:10px;background:#fff7ed;border-radius:6px;font-size:12px;color:#92400e">
- 还有 ${pending.length} 块待确认:${pending.map(b => b.title).join('、')}
- </div>`;
- }
- if (appState.confirmedBlocks.size === bidBlocks.length) {
- html += '<p style="margin-top:16px;color:#059669;font-weight:600">✓ 全部块已确认,可导出 Word</p>';
- }
- return html;
- }
- function selectBlock(id) {
- appState.activeBlockId = id;
- renderBidTask();
- }
- function highlightDocLine(i) {
- const block = bidBlocks.find(b => b.docLine === i);
- if (block) appState.activeBlockId = block.id;
- renderBidTask();
- }
- function confirmBlock(id) {
- appState.confirmedBlocks.add(id);
- const idx = bidBlocks.findIndex(b => b.id === id);
- if (idx < bidBlocks.length - 1) appState.activeBlockId = bidBlocks[idx + 1].id;
- appState.renewalTasks.find(x => x.id === 'r5').done = appState.confirmedBlocks.size === bidBlocks.length;
- toast('已确认:' + bidBlocks.find(b => b.id === id).title);
- renderBidTask();
- renderBidWorkbench();
- renderProject();
- renderTenderBidList();
- if (location.hash === '#renewal-detail') renderRenewalHub();
- }
- function confirmAllBlocks() {
- confirmBlock(appState.activeBlockId);
- }
- function exportWord() {
- if (appState.confirmedBlocks.size < bidBlocks.length) return;
- appState.bidExported = true;
- document.getElementById('btn-register-bid').disabled = false;
- renderTenderBidList();
- toast('Word 导出成功 · 请完成投标登记以开启开标监控');
- }
- function goRegisterBid() {
- if (!appState.bidExported && appState.confirmedBlocks.size < bidBlocks.length) {
- toast('请先完成标书确认并导出');
- return;
- }
- appState.bidExported = true;
- navigate('bid-result');
- setResultTab('register');
- prefillRegisterForm();
- toast('请填写开标信息并开启监控');
- }
- function setResultTab(tab) {
- appState.resultTab = tab;
- document.querySelectorAll('.page-tab').forEach(t => {
- t.classList.toggle('active', t.dataset.tab === tab);
- });
- document.getElementById('result-panel-register').classList.toggle('hidden', tab !== 'register');
- document.getElementById('result-panel-monitor').classList.toggle('hidden', tab !== 'monitor');
- document.getElementById('result-panel-win').classList.toggle('hidden', tab !== 'win');
- renderBidResult();
- }
- function prefillRegisterForm() {
- const t = getRenewalTender();
- document.getElementById('reg-project').value = renewalDemo.displayName;
- document.getElementById('reg-tender-no').value = t ? 'SH-GP-2026-0892' : '';
- document.getElementById('reg-submit-date').value = '2026-06-20';
- document.getElementById('reg-open-date').value = '2026-06-28';
- document.getElementById('reg-open-time').value = '14:00';
- document.getElementById('reg-bid-amount').value = '1668';
- document.getElementById('reg-monitor-url').value = 'https://www.zfcg.sh.gov.cn/portal/detail?resultId=demo-0892';
- }
- function renderRegisterForm() {
- if (appState.registerFormBuilt) return;
- appState.registerFormBuilt = true;
- document.getElementById('register-form').innerHTML = `
- <label>项目名称</label><input type="text" id="reg-project" placeholder="关联在管/投标项目" />
- <label>招标编号</label><input type="text" id="reg-tender-no" placeholder="如 SH-GP-2026-xxxx" />
- <label>投标提交日</label><input type="date" id="reg-submit-date" />
- <label>开标日期</label><input type="date" id="reg-open-date" />
- <label>开标时间</label><input type="time" id="reg-open-time" value="14:00" />
- <label>投标报价(万元/年)</label><input type="number" id="reg-bid-amount" placeholder="615" />
- <label>结果监控 URL</label><input type="url" id="reg-monitor-url" placeholder="政府采购网开标结果页链接" />
- <label>监控说明</label><textarea id="reg-note" placeholder="可选:备注包件、标段等">系统将每 30 分钟自动检查该 URL,开标结果发布后抓取并推送。</textarea>
- `;
- }
- function submitBidRegistration() {
- const project = document.getElementById('reg-project').value.trim();
- const openDate = document.getElementById('reg-open-date').value;
- if (!project || !openDate) { toast('请填写项目名称和开标日期'); return; }
- const sub = {
- id: 'sub-' + Date.now(),
- projectName: project,
- projectId: renewalDemo.id,
- tenderNo: document.getElementById('reg-tender-no').value,
- submitDate: document.getElementById('reg-submit-date').value,
- openBidDate: openDate,
- openBidTime: document.getElementById('reg-open-time').value || '14:00',
- bidAmount: parseFloat(document.getElementById('reg-bid-amount').value) * 10000 || 6150000,
- monitorUrl: document.getElementById('reg-monitor-url').value,
- status: 'monitoring',
- monitorSince: '2026-06-08 10:00',
- lastCheck: '2026-06-08 10:30',
- checkCount: 12,
- result: null,
- pushes: [],
- timeline: [{ time: '2026-06-08 10:00', text: '投标登记完成,开标监控已开启' }],
- };
- appState.bidSubmissions = appState.bidSubmissions.filter(s => s.projectId !== renewalDemo.id);
- appState.bidSubmissions.unshift(sub);
- appState.selectedSubmissionId = sub.id;
- appState.renewalTasks.find(x => x.id === 'r6').done = true;
- toast('投标已登记 · 开标监控已开启 · 距开标 ' + daysUntilOpen(sub) + ' 天');
- setResultTab('monitor');
- renderDashboard();
- renderMarket();
- renderProject();
- renderTenderBidList();
- if (location.hash === '#renewal-detail') renderRenewalHub();
- }
- function daysUntilOpen(sub) {
- if (!sub?.openBidDate) return '—';
- return Math.ceil((new Date(sub.openBidDate) - DEMO_TODAY) / 86400000);
- }
- function renderMonitorList() {
- const list = appState.bidSubmissions;
- const monitoring = list.filter(s => s.status === 'monitoring').length;
- const fetched = list.filter(s => s.status === 'result_fetched').length;
- const pushed = list.filter(s => s.status === 'pushed').length;
- document.getElementById('monitor-stats').innerHTML = `
- <div class="pool-stat ${monitoring ? 'highlight' : ''}"><span>监控中</span><b>${monitoring}</b></div>
- <div class="pool-stat"><span>待登记结果</span><b>${fetched}</b></div>
- <div class="pool-stat"><span>已推送</span><b>${pushed}</b></div>
- `;
- document.getElementById('monitor-tab-badge').textContent = monitoring ? ` (${monitoring})` : '';
- document.getElementById('btn-simulate-result').style.display = monitoring ? 'inline-block' : 'none';
- if (!list.length) {
- document.getElementById('monitor-list').innerHTML = `
- <div class="empty-shell" style="border:none;padding:24px">
- <strong>暂无监控任务</strong>
- <p>请先在「投标登记」中提交投标信息</p>
- <button type="button" class="btn btn-primary" style="margin-top:12px" onclick="setResultTab('register')">去登记 →</button>
- </div>`;
- return;
- }
- document.getElementById('monitor-list').innerHTML = list.map(s => {
- const statusMap = {
- monitoring: ['监控中', 'tag-warn', true],
- result_fetched: ['结果已抓取', 'tag-danger', false],
- registered: ['待推送', 'tag-warn', false],
- pushed: ['已推送', 'tag', false],
- };
- const [label, tagCls, pulsing] = statusMap[s.status] || ['—', 'tag', false];
- return `<div class="monitor-card ${s.status === 'result_fetched' ? 'won' : ''} ${appState.selectedSubmissionId === s.id ? 'active' : ''}" onclick="selectSubmission('${s.id}')">
- <div class="monitor-head">
- <strong>${s.projectName}</strong>
- <span class="tag ${tagCls}">${label}</span>
- </div>
- <div class="monitor-meta">
- <span>招标编号 ${s.tenderNo || '—'}</span>
- <span>开标 ${s.openBidDate} ${s.openBidTime}</span>
- <span>报价 ${formatMoney(s.bidAmount)}</span>
- ${s.status === 'monitoring' ? `<span>距开标 <strong>${daysUntilOpen(s)}</strong> 天</span>` : ''}
- </div>
- <div class="monitor-status">
- ${pulsing ? '<span class="pulse-dot"></span>' : ''}
- <span>最近检查 ${s.lastCheck} · 已检查 ${s.checkCount} 次</span>
- </div>
- ${s.result ? `<div style="margin-top:10px;font-size:13px;color:#059669"><strong>${s.result.won ? '中标' : '未中标'}</strong> · 得分 ${s.result.score} · 排名 ${s.result.rank}</div>` : ''}
- <div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap" onclick="event.stopPropagation()">
- ${s.status === 'monitoring' ? `<button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="simulateOpenBidResult('${s.id}')">模拟结果发布</button>` : ''}
- ${s.status === 'result_fetched' ? `<button type="button" class="btn btn-primary" style="font-size:12px;padding:4px 10px" onclick="goWinRegister('${s.id}')">中标登记 →</button>` : ''}
- ${s.status === 'pushed' ? `<button type="button" class="btn" style="font-size:12px;padding:4px 10px" onclick="goWinRegister('${s.id}')">查看推送</button>` : ''}
- </div>
- </div>`;
- }).join('');
- }
- function selectSubmission(id) {
- appState.selectedSubmissionId = id;
- renderMonitorList();
- }
- function simulateOpenBidResult(id) {
- const sub = appState.bidSubmissions.find(s => s.id === (id || appState.selectedSubmissionId || appState.bidSubmissions.find(x => x.status === 'monitoring')?.id));
- if (!sub || sub.status !== 'monitoring') { toast('没有监控中的投标'); return; }
- sub.status = 'result_fetched';
- sub.lastCheck = '2026-06-08 10:35';
- sub.checkCount += 1;
- sub.result = { won: true, score: 92.5, rank: 1, publishDate: '2026-06-08', winner: '上海申勤物业发展有限公司' };
- sub.timeline.push({ time: '2026-06-08 10:35', text: '系统自动抓取开标结果:中标,得分 92.5' });
- appState.selectedSubmissionId = sub.id;
- toast('开标结果已抓取:中标!请完成中标登记与推送');
- renderMonitorList();
- renderDashboard();
- renderMarket();
- renderProject();
- renderTenderBidList();
- if (location.hash === '#renewal-detail') renderRenewalHub();
- setTimeout(() => { setResultTab('win'); goWinRegister(sub.id); }, 500);
- }
- function goWinRegister(id) {
- appState.selectedSubmissionId = id;
- setResultTab('win');
- renderWinPanel();
- }
- function renderWinPanel() {
- const sub = appState.bidSubmissions.find(s => s.id === appState.selectedSubmissionId);
- const el = document.getElementById('win-content');
- if (!sub) {
- el.innerHTML = `<div class="empty-shell"><strong>暂无待登记的中标结果</strong><p>请先完成投标登记并等待开标结果抓取</p>
- <button type="button" class="btn" onclick="setResultTab('monitor')">查看监控 →</button></div>`;
- return;
- }
- if (!sub.result) {
- el.innerHTML = `<div class="empty-shell"><strong>${sub.projectName}</strong><p>开标 ${sub.openBidDate} · 监控中,结果尚未发布</p>
- <button type="button" class="btn btn-primary" onclick="simulateOpenBidResult('${sub.id}')">模拟结果发布</button></div>`;
- return;
- }
- const won = sub.result.won;
- el.innerHTML = `
- <div class="result-banner ${won ? '' : 'lost'}">
- <strong>${won ? '🎉 中标' : '未中标'}</strong> · ${sub.projectName}
- <div style="font-size:13px;margin-top:6px;color:var(--text2)">
- 得分 ${sub.result.score} · 排名 ${sub.result.rank} · 结果发布 ${sub.result.publishDate}
- ${won ? ' · 中标单位:' + sub.result.winner : ''}
- </div>
- </div>
- ${won && sub.status !== 'pushed' ? `
- <div class="card">
- <div class="card-head">中标登记确认</div>
- <div class="card-body">
- <div class="form-grid">
- <label>中标日期</label><input type="date" id="win-date" value="${sub.result.publishDate}" />
- <label>中标得分</label><input type="text" id="win-score" value="${sub.result.score}" />
- <label>中标金额(万元/年)</label><input type="number" id="win-amount" value="${(sub.bidAmount/10000).toFixed(0)}" />
- <label>承接情况</label><select id="win-type"><option value="存续">存续(续标)</option><option value="新增">新增</option></select>
- <label>备注</label><textarea id="win-note">续标项目,合同衔接原公安分局机关办公区物业管理。</textarea>
- </div>
- <button type="button" class="btn btn-primary" style="margin-top:16px" onclick="confirmWinAndPush('${sub.id}')">确认中标 · 一键推送</button>
- </div>
- </div>` : ''}
- ${sub.status === 'pushed' ? `
- <div class="card">
- <div class="card-head">部门推送状态 <span class="tag" style="float:right">已完成</span></div>
- <div class="card-body">
- <div class="push-grid">
- ${sub.pushes.map(p => `<div class="push-item done"><strong>${p.dept}</strong>${p.action}<br><span style="color:var(--text3)">${p.time}</span></div>`).join('')}
- </div>
- </div>
- </div>` : `
- <div class="card" id="push-preview-card" style="${sub.status === 'pushed' ? '' : 'opacity:0.6'}">
- <div class="card-head">推送预览(确认后发送)</div>
- <div class="card-body">
- <div class="push-grid">
- <div class="push-item"><strong>运营部</strong>前介方案待办</div>
- <div class="push-item"><strong>人力资源</strong>招聘需求草稿</div>
- <div class="push-item"><strong>财务部</strong>收入确认 · 开票日历</div>
- <div class="push-item"><strong>条线主管</strong>定编定岗通知</div>
- </div>
- </div>
- </div>`}
- <div class="card">
- <div class="card-head">流程时间线</div>
- <div class="card-body"><ul class="timeline">${sub.timeline.map(t => `<li class="done">${t.time} · ${t.text}</li>`).join('')}</ul></div>
- </div>
- `;
- }
- function confirmWinAndPush(id) {
- const sub = appState.bidSubmissions.find(s => s.id === id);
- if (!sub) return;
- const now = '2026-06-08 10:40';
- sub.status = 'pushed';
- sub.pushes = [
- { dept: '运营部', action: '前介看板新增卡片', time: now },
- { dept: '人力资源', action: '招聘联动 · 缺口草稿', time: now },
- { dept: '财务部', action: '收入科目 · 开票节点', time: now },
- { dept: '条线主管', action: '定编方案待确认', time: now },
- ];
- sub.timeline.push(
- { time: now, text: '市场部确认中标登记' },
- { time: now, text: '已推送运营 / HR / 财务 / 条线(4 部门)' },
- );
- appState.renewalTasks.find(x => x.id === 'r7').done = true;
- toast('中标已登记 · 已推送 4 个职能部门');
- renderWinPanel();
- renderMonitorList();
- renderDashboard();
- renderMarket();
- renderProject();
- renderTenderBidList();
- if (location.hash === '#renewal-detail') renderRenewalHub();
- }
- function renderBidResult() {
- if (!document.getElementById('register-form').innerHTML) renderRegisterForm();
- renderMonitorList();
- renderWinPanel();
- }
- function renderGisList(filter) {
- filter = (filter || '').toLowerCase();
- const items = [{ ...renewalDemo, renew: true }, ...otherProjects.map(p => ({ ...p, renew: false }))];
- const filtered = items.filter(p => !filter || p.displayName.includes(filter) || (p.address && p.address.includes(filter)));
- document.getElementById('gis-count').textContent = `共 ${filtered.length} 条`;
- document.getElementById('gis-list').innerHTML = filtered.map(p => `
- <button type="button" class="gis-item ${appState.gisSelectedId === (p.id || 'renewal-demo') ? 'on' : ''}" onclick="openGisProject('${p.id || 'renewal-demo'}')">
- <b>${p.displayName} ${p.renew ? '<span class="renew-badge">· 续标 ' + daysUntil(renewalDemo.contractEnd) + '天</span>' : ''}</b>
- <span>${p.address || '青浦区'}</span>
- </button>
- `).join('');
- }
- function openGisProject(id) {
- appState.gisSelectedId = id;
- const p = id === 'renewal-demo' ? renewalDemo : otherProjects.find(x => x.id === id) || renewalDemo;
- const days = p.contractEnd ? daysUntil(p.contractEnd) : '—';
- document.getElementById('gis-card-title').textContent = p.displayName;
- document.getElementById('gis-card-body').innerHTML = `
- <dt>甲方名称</dt><dd>${p.clientName || p.displayName}</dd>
- <dt>项目地址</dt><dd>${p.address || '—'}</dd>
- <dt>合同起止</dt><dd>${p.contractPeriod || p.contractEnd || '—'}${p.id === renewalDemo.id ? ` <span class="tag tag-danger">剩 ${days} 天</span>` : ''}</dd>
- <dt>在管服务</dt><dd>${p.serviceScope || '—'}</dd>
- <dt>金蝶编码</dt><dd>${p.kingdeeCode || '—'}</dd>
- `;
- document.getElementById('gis-btn-renewal').onclick = () => { closeGisCard(); startRenewal(); };
- document.getElementById('gis-card').classList.remove('hidden');
- document.querySelectorAll('.pin-project').forEach(pin => {
- pin.style.transform = pin.dataset.id === id ? 'translate(-50%,-50%) scale(1.3)' : 'translate(-50%,-50%) scale(1)';
- pin.style.zIndex = pin.dataset.id === id ? '5' : '2';
- });
- renderGisList(document.getElementById('gis-search')?.value);
- }
- function closeGisCard() {
- document.getElementById('gis-card').classList.add('hidden');
- }
- function startRenewal() {
- openRenewalDetail('renewal-demo');
- }
- const routes = {
- dashboard: { view: 'view-dashboard', nav: 'dashboard', render: renderDashboard },
- gis: { view: 'view-gis', nav: 'gis', render: () => { renderGisList(); updateGisClock(); bindGisOnce(); }, fullscreen: true },
- projects: { view: 'view-projects', nav: 'projects', render: renderProjectList },
- project: { view: 'view-project', nav: 'projects', render: renderProject },
- renewal: { view: 'view-renewal', nav: 'renewal', render: renderRenewalList },
- 'renewal-detail': { view: 'view-renewal-detail', nav: 'renewal', render: renderRenewalHub },
- market: { view: 'view-market', nav: 'market', render: renderMarket },
- 'tender-pool': { view: 'view-tender-pool', nav: 'tender-pool', render: renderTenderPool },
- 'tender-bids': { view: 'view-tender-bids', nav: 'tender-pool', render: renderTenderBidList },
- 'tender-reserve': { view: 'view-tender-reserve', nav: 'tender-pool', render: renderTenderReserveList },
- 'bid-workbench': { view: 'view-bid-workbench', nav: 'bid-workbench', render: renderBidWorkbench },
- 'bid-task': { view: 'view-bid-task', nav: 'bid-workbench', render: renderBidTask },
- 'bid-result': { view: 'view-bid-result', nav: 'bid-result', render: () => setResultTab(appState.resultTab || 'register') },
- };
- function navigate(name) {
- location.hash = name;
- }
- function updateGisClock() {
- const el = document.getElementById('gis-clock');
- if (el) el.textContent = new Date().toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit' });
- }
- let gisBound = false;
- function bindGisOnce() {
- if (gisBound) return;
- gisBound = true;
- document.getElementById('gis-search')?.addEventListener('input', e => renderGisList(e.target.value));
- document.getElementById('layer-renew')?.addEventListener('change', e => {
- document.getElementById('gis-zone-renew').style.opacity = e.target.checked ? '1' : '0.2';
- document.querySelectorAll('.pin-project.renew').forEach(p => { p.style.opacity = e.target.checked ? '1' : '0.35'; });
- });
- document.querySelectorAll('.pin-project').forEach(pin => {
- pin.onclick = () => openGisProject(pin.dataset.id);
- });
- }
- function route() {
- const hash = (location.hash || '#dashboard').slice(1);
- const r = routes[hash] || routes.dashboard;
- document.querySelectorAll('.view, .view-gis').forEach(v => v.classList.add('hidden'));
- document.getElementById('app-shell').classList.toggle('hidden', !!r.fullscreen);
- document.getElementById(r.view)?.classList.remove('hidden');
- document.querySelectorAll('#main-nav a[data-route]').forEach(a => {
- a.classList.toggle('active', a.dataset.route === r.nav);
- });
- if (r.render) r.render();
- }
- window.addEventListener('hashchange', route);
- window.addEventListener('load', route);
- </script>
- </body>
- </html>
|