From c93bf9ef1133ca0bb0126085e655be4b06578096 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Mon, 2 Feb 2026 14:32:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0admin=20=E5=92=8C=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/settings.cpython-312.pyc | Bin 2654 -> 3855 bytes .../__pycache__/settings.cpython-313.pyc | Bin 2654 -> 3855 bytes .../config/__pycache__/urls.cpython-312.pyc | Bin 462 -> 1209 bytes .../config/__pycache__/urls.cpython-313.pyc | Bin 462 -> 1174 bytes backend/config/settings.py | 53 +++++ backend/config/urls.py | 12 ++ backend/requirements.txt | 10 + .../shop/__pycache__/admin.cpython-312.pyc | Bin 8089 -> 8184 bytes .../shop/__pycache__/admin.cpython-313.pyc | Bin 8271 -> 8348 bytes .../shop/__pycache__/models.cpython-312.pyc | Bin 11688 -> 14282 bytes .../shop/__pycache__/models.cpython-313.pyc | Bin 11594 -> 14106 bytes .../__pycache__/serializers.cpython-312.pyc | Bin 6647 -> 8099 bytes .../__pycache__/serializers.cpython-313.pyc | Bin 6995 -> 8494 bytes backend/shop/__pycache__/urls.cpython-312.pyc | Bin 908 -> 1006 bytes backend/shop/__pycache__/urls.cpython-313.pyc | Bin 902 -> 1000 bytes .../shop/__pycache__/views.cpython-312.pyc | Bin 6361 -> 6770 bytes .../shop/__pycache__/views.cpython-313.pyc | Bin 6377 -> 6806 bytes backend/shop/admin.py | 31 +-- ..._content_service_delivery_time_and_more.py | 55 ++++++ backend/shop/models.py | 37 ++++ backend/shop/serializers.py | 34 +++- backend/shop/urls.py | 3 +- backend/shop/views.py | 11 +- frontend/src/api.js | 1 + frontend/src/pages/Home.jsx | 27 ++- frontend/src/pages/ServiceDetail.jsx | 186 +++++++++++++++--- 26 files changed, 407 insertions(+), 53 deletions(-) create mode 100644 backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py diff --git a/backend/config/__pycache__/settings.cpython-312.pyc b/backend/config/__pycache__/settings.cpython-312.pyc index 0210904b45b986a6bb912003c1d40b685d8fa1e4..86e1e326bf0ebdfe10a1cfe7e65645988680a70c 100644 GIT binary patch delta 1347 zcmaJ=-EZ1f6t^J(0tq1jHEBMmOVSiK3$YCZ*h^;_C-KVHY}0I+CX3|~>XiV(ur|Dq zO8cnx(nXP`YKk^hd+0;Es#5yUF4|wPm-fxO4^yQzVUJ{c+2gK(RMS-L+WzUB-#Pc# z=Xb7u^W5?Je;FFGV`x3#cGSnC81}p2oGw?-_zU2>{=b`fEATcP7`8r+taFA{z}L?L zqJR}JaDLysJ^_4S2F!wfaG`)BoI{UsUEDVV|GsT~5|H2$mfC4mNKoEpL z7%YJZi0(U79K`nf)~B{j`t(L2)8=Gn|uR%-KY54xLB~Y&9OSy^~VC%KA zv~exJl_Sm26X%!NY(iEfZk1263YTCd33^XiHY>04DOuoHSxBe4QC@-*r|}FgCYypJ z{VQ0XL~6F~?;d}(bM*PcqdQMu{dE6m@75nreth-pF`E(Um;NVy^yqHu=iSz~pP|I# zZ@xUf_d}zf4p0HAF;mTJHD$A!+sfZ4SFZ;u`RbOcX==ID)Bi?UgAU{V>Az~S9#HB6nZ3Hyvi#=ic4hUywdDS2S>WJAxT#e3662;WG0Tk`ei3;eF_q*4UL5UR1JzBJr{;q*kQ*_#4G)J-XNW`ir`^ z6KuX&>ej(w>+pMsFG6djs&3_~+YMK>@Ufo>Q3Ml;5llEt_82CF7zQz+9z#c$2quW~ z!ogF92@?!WBZeUx!$zI(5ngcL$Qz!BNwW^UtZh?*^VVckH_J%-!i1&bb{&+lKWI-M;yvZ>()Lc}&kSS9=hf zW^sMaulr_CtbNA5whMF4>+Z|?@ELuI&?hcGKl_e;ZbqN*zZ{#?iCNw2Lzz>SlaZlu zRKnBtVeSRp9n@X!m$ngobXIp?IK5}}i*Dr4bZ%b<&!3D9dhBhpbJ%h+;I^A?S*mO3 G%l`o%VZZ4B delta 123 zcmeB|yC- z+5D7QotaT+GcUU#Ba^1Y<_2y##>uaF(zy(Q+8BYjm}hbfuP-~75K{x+CyB`$cz3cl Jauw+VWdJ!l8leCH diff --git a/backend/config/__pycache__/settings.cpython-313.pyc b/backend/config/__pycache__/settings.cpython-313.pyc index d702cc429ba711da8e48a6b2af047a02a7a8f1e7..707d1a555cab2d2669eae7366050a82532786a8f 100644 GIT binary patch delta 1347 zcmaJ=-EZ1f6t^J(0tq1jHEBMmOVSiKi?Iy^*h^;_C-KJDd`+`yn=BSc>XiVpVQqLJ zmG)8XrHdj>RUbB0d+0;Es#5x}F53UlzIpdysS@6;uz*kepaY#RSCUx5XNudLoR4*R5q*S;{HR+6VGT-+f?*g zX`^1)Xp{@ufnp6>ao@o$8-ur?ORpE|>c*y8F6q$LbDDwwY@w>PQl(H+DXvkkf{m-C z?E-0qzNE0iBz2k3okG_0(bo=L*zuZ6Czxn5rpI$zF%;m(!rT>W^J-XBWb+3K*3zT^L-PgzW zerygh6iw01IjyAEl})X%UAkV?u2Hp;wyo;ATCKEniYu3^*SX40(-lG;hCcs@UP^p| zx^opvrCLg@TvIDs(1*6@s$NI^U#)6t6G>LB!11E0*UE*REQ)p+u}C`Lpe)KM0lJa` z;3Y9yk8C%Q~9aa1PKiuV~NLNLoHEsQp~yRRacFiS9rMBfh+3>`%|4A~qp<1qpi zU|y#)ED=K`7@BMjo8dSSi4n0dNxGmVN<%9{BVbJkn-DP}O4Do5x@?*;6XGT$x)7zw z0q6vxEGYeHcI`ERO9~Qnbe+7Ef;K*z%1VHoJvg-c@icS+8J%`sk+NACdV0=^m=b`J z6eR%N5*nbgB5`S9JuAJ3PD4)MWsc9MIO+A=pe>(S$)=L~zd5#?AEGg#KOEB^?7usd zwhxb6cHMu??0-M>w6*Ir+-L60K6fwdx;hSQaOBpF=K~WRtHoz|hIu-}*er(|3qd0= ze_|gn_jNp&d(rS-Hb&1FGlVg9`PtcbjB|6wRPe>bv_Z@p{s79Hv7L;KOrjFLP5|>R z8Q!qr@xE}38RPSY_rmEtZ(Q^uf3AB6x_JI%V%X>GSly$xlOeCua?_@*qc8slK7zVi delta 123 zcmeB|yC- z+5D7QotaT+GcUU#Ba^1Y<_2y##>uaF(zy(Q+8BYjm}hbfuP-~75K{x+CyB`$cz3cl Jauw+VWdKHZ8n*xd diff --git a/backend/config/__pycache__/urls.cpython-312.pyc b/backend/config/__pycache__/urls.cpython-312.pyc index 7669ebed8abd59ec3d412f64ea829d9ba9fdf852..0a53fcfa05f9b2c9ae179bf7537f3346faa75a59 100644 GIT binary patch literal 1209 zcmah|O-~a+7@lpX?Y7%ezHA#l)SwWH(oF!3Ap${AVl*NJl&ptl=nn1H-EA|oh0qgv zk)xd8z)~)B2G(hj*U$dEU>R?8i_j0NAO{ZfieX z0DR?6<2VX)&fW?D>;V<1f({Hp5d@B1y324YZUZXN@!h(|@G4#hL)~YHis)dE?l%HT zz>pNl2r5AVxBzymUM%E#8jYHFuCIw3=zz_|kCW$P7OLU_n~0y^G(Xb$D4Wai0(ryAb0BQ4z0Cdy*xU18=A<#B|jHg2`cl^SbWN%1GL_6q^OkwJcRH*vh8th z78+u1(ZHsaBKf@aINoiysX1h3@`-fbw1~Dw_ygP0ImrhhkTJy5O0(E?;^j=VnZ53e zov42r;^XQCm8_>|^9v<5*v6EIyr7>O{*Ty@Y8JL3?`8WCP4OUGVlI0E3lo#tp1FH> zSLW@&((=smgW1$d@{ui_VO{$@(RU-$~jE1v6VrlJa@h=l~ueU>lhJJ z)-|?oPD>#zl|@9|L~PDGBO_}QP{G@J4RkD>UfG=V)gk?Lq9Nc>J|jy z3%Gm&2ATZ=hEKp+1+0Dd0*`+u_U7i^#O}o2^zQUAyz&`}JCWV!e(!O|;4vJkhxC_Y zqvhCeEjC(-jULCw4{ulD9FOcRN5;#M8@0%IB{F`Ptw!dL^eTMLrM_}>tQ@^wi;h*I zV~4BN=>4N-Rk(5*7Q?P>=`;dh@b%QIsZTJxdu#t%t!JdtGjh0En_R3+E>^pe6_|Ya N@YD@pC+TM!`~sz?IFbMW delta 249 zcmdnVd5&4*G%qg~0}!lKYRFs;q#uJgFu)FFe6C=asG$(U#E{CI!ko&O1=aym1fjEd zQ28LMa(K&@FBtS&&##l3J8kJh`1YS%ME}GRX8|Qy}qy enURt4E`!2-2Gz-YEXq6|Sef~m8o7%=&IABOfi8{! diff --git a/backend/config/__pycache__/urls.cpython-313.pyc b/backend/config/__pycache__/urls.cpython-313.pyc index 0e17fa997919576314959cd1b7351a364b6f3321..8d80a5beb3ace18a9e741292736bc7b2c58398c2 100644 GIT binary patch literal 1174 zcmaJ=O-~a+7@qA;+ika{eAzbmMNo-F=_Y&_A&OB^LTf~KK}~GZ4Beq!Y`4wK7Gh7B znCMXsM*o6$5Bvm=ESOX$Asjq;!*DinW?Q62B+&p-JACFz;M22l;7bl6NHRk60)d*3Y!~&UFY( zwp}A^s8h0+Cz4bDt>v2Nlw@@MR>pFX|0=tMQuCAUTtFY{tlPpD+Y~y>C+N;8LxAhf z|E=Tf@ZYS_GvynN0zkTpy~W;C-*+KNNz9MTie;yK4AziSVnNF;l{38pmvv>z*tfOKP6OZrj;B{%2^p@b_);}t!?YeO4Va3dswJO$N6Yh0iA=rC>78?9T7u#gZ@M`M0( z=h7&5>lJJ}c~Y%9_f!3>%Unfvv6?PaZHHLPMCKR4)HW#^A&@h~-$`@r3JC}%aeR8k zYdhV#8WI%I6DC>7)8iXTuknc~QAEO!Sj0cVAhjIKps2-yh~|ZesodqSz`?|(%s+eQ z*3ujc8;ds==Wpki^m|NgV|`&^@dG&Ic>r6`mMA7vTg3!do)?j=&9HU2eKqd zdth)MT;S^-7}*EQE?E8%0RGVH)H$ zPL4E_V>`*Q-Q>j9RTs_**~#tr9Gr!4q{5ox1k=y0gSwwc{TF|#x}3Kzt|iYbq7Ybo;0sNbotY#yHJ0TJ&*zHC8v4g FzW}7GEKvXe delta 249 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 R)aqFQ8psZQi*8;N{tt(L&bR;o diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc index 3935e94de991974664c40f28a21bcdae13454278..e8e6f5ec71608bc1705af58259204829e3923ac4 100644 GIT binary patch delta 3376 zcmaJ@Yiv}<6~1$K_wMyR?!LUXch`^g6V@2>kZ}SDFNY*$O&;!TBT7tGi}%{uXxYVc zcilpf?9h^=l_1b*B$N9ho@2s&iaW&HYl;O07|u%3(nj zR7-zV)JAO}ORBZs9(7Ph)JdIDg(^`Obw%CO9raL8)JwgbFZcVX5A3S?tD}DEFX?J% zjcOYtKkUXim;O?9<+f@cT!9j(P#h~Tf@MaX&?2bLpr9&)@+MPJOG9AwGz*LVHLgvh zsN*~r@Z5veGH+995ZX}Uw>Efy;a!H&SQ*j1WJDkEtC!(7RrsDId_VAOmf?phd}Rs0 z7Wj3`@S7`q=MsJZ_`zlPEhBOy#Oh6Lt0SUednuF9Qacj|lWD{GqxgYbDo%H&Q^~Xz zA%-=P)DNZNhp+801&HK^5YZ|YGan^xcE`MdgqULaiOq*<1UDPC1f@3KWT!1ZBYtL+ z*7d9bMMR_#)OP~p8~}6YzMMaM<^JxArTEyyrEX3#b4ptQZA3oSwbFGs3qJvJH; z8WEb$zaT2%%=3tRUW-C4zAp#VE;9U>jSk)IuS3Mrcbz^z`CYXJW7 z*sa-{=jY$PT6T*wo-Wh4_Y3E*%uatc_x`c@4?c}p=u_;39F%ZCx`llrr-yMte9)5n z_WyAo3F{EnBWysx5E|AbkG)PeqH4MI^8zH{<|N?GeRN}f@?A(`a}v}vz`nBe zjz5QUZ9~|O&?xSU26n>!ytB#ck$$nen4~Yj(Fm}A*V>dgy#s_66g;2Fsd9$ci1%*m^gMAMQ)TT;< zwv-*L`A@Zj&cSUQs&g|@l_xBmDwIm9K(SOFTX3$2bG=V+U8+y5R!!@7VY2x_tl`O4 z3sN0VDid|90qXg&u$546n-B>aZZ)3L^g|leGwEV_#lR=XhF<1Y)=K>J?Ys7zvV%zc zd~57it`?%PE3Q7$#(wQ-^JC`2NybBL$Qhc@Xfi#*9=iTQM%lD`bK3}Zm@)_6GHi$9 z$wW-Y33U@_!{IRHgTBt{Jk9PDsC3+r385efH39ab2j{rzd9n3%&_aIWP;?AwB0$+1 z9X8rngQvbKR;rkkZS&T1+W^aY2io=l1xj4S-jEN(buE^osmG46``(RYjMe+v19+-= z*3O^&{oEHf@3R~8Coe__tzmt>{iKgg`kIvRP%5tLTU!p~ve`_U-SCAX<6uV3-=E5~ zWEP|hGO>Df*Uv%Jn*k<-dy4lPC45T>Px@w*_V-M8T)})OmX9Z9Tv}e%CGlWNsX+C$ zPX9j%sbVd)N69PfTD-KKEH950PvkQ=a0|c_5udw$aqjqa z$SPbF#X))J?B^dYOkZA@KKI?!c?bkB9l%Xjt$o&wNyf<8rsST zm|HZ)!oiwXilH4uMP=g0SApS-#dv6jv{Wh<0|(v$rjD9<1DsZvTCD`hC4eThe3k^QD zA+)+!q4vT}e3V>bcS6mZ4xzId0K?9YWU&U&-JpS^f*o`Ufh)}P4=7(@8|&NO;FhSS zp8^!i`_zTG6CXTzb6e;q&}qn{xp+F8%pRtd`S^(3YHsSn{Cn4nRxsxk9P%prV||27 zv2W{Z_u}xSJKVPN4zEWRza=iBwBl898G8{fv6mZ;*xSm@NGqFZcz)cGOAlvKiS9T) z#wcEJ)CypDO8SI0l%er#hUz?T4Cx>~-E@kNd5X21;uD5C5flWxWvLtCw*ZEi%uo-u z&SJ}~C$kzv9x(PMB&Wtk8iQ{ZUm-HpMv{6~qs7-0e{%49j$b-F$@p4AI2vCDo;^c; z7Cg$Ov~BbzG%+ppUciEh5OP-t-xVT&-wGS=3hnGajoa(DJ`zmiHBzt%qV?UzV~wX= zGbH>-RETtD3)7lf$r1K`Q&Zeeq*GfK1^7TJih+U%QlYzSQ#C_^k3>6g_ALtVK~AWE tibAl=vCokDN8&C5q$7(0ev#Es;HttN)*SZvgNuT%sIP^5CK6xQe*t_c@1Fnw delta 3329 zcma)8ZERE58NSE9zP|qUwd2@MNMeV4IY9U*q0lA}pn;N)s^gL{x=_y~PB1k#JvUkD z%9OBbv_A?0og$&GS}9dOGF9m$stt-(rb(OjZ;p~F(PizJL_+!JB~;s=O?%&SW8!qm z#Fg^+ocDd+=RKeA$-BG%+~!_%x$FXb`*)6|{t~+64w3w&`o>XFwRB1yR%-2#soY_s zwhlYBcQ~k{!%3YTF6xR1s-Q}pZt8|#Yp18fOT9}rg(|AtM-JAY?#eIiFZ5O0DvT0j z970g-t55>y#V;gO3#y|`P@TYBZ`g{l*=+P^5d53i-O`e_K6^7Uj6B!{!ESJFDGoE* zh2Z8T7cE;Bv91vDfQa`oMaq{&94kZ=5b-^x$eMx(8zY|0euyl2%*cvDWLuwYMfw1U zls=|N<+6ypLL>+xp~n=d8j@mR_Nl4n`oBzJBDrCeslsjkiJ$%6vTn}xgmQ#6?5wn-GJ;KH7@4cFg=d|E zpUp}~?R-dR>%XJ~*dc2_sbM#)EjzX%Z5sd#?Fnq-efV~HX!mZvKY#YK!q7Mak0153 z4RSDw(Yk#wozRZQwbYN3x=a%%d5de(Cs|U?4C1`F?$RtA|1%3D;BBEB5SkEhiFIo# zlTKxl8r_Jd2*P)!xfyK;HEf4%(3mE6ahjxy-L}<@%BoGZH<`AYREO$RT}y4ZYEeC# z$X3%iWeIsVl#Z-SbLrdB3?AyF;NPQZi1z{JH9MR|aCXdUj zFAratRo7xKDk0#wwE;HbsAIo!Jnag56zTN?#@X1*Po?7Q4s;>A9brF$F%PCWw>9m; zej5TG5bZ29i;h<}+7PpGJ(pfwNFmlz|TLZzH*vZb?mA(U^sx` zC}P;b=ETxTT~It-MINY#P}ALg?jjo$IO>5C=P9x&s!ugVMGF*f`K7+p-HPq3JKgTxQ}q3eC_HX`vW6=&Bx z)#Qil51u_DPMGcX{)wDmc4bFR0=l|6d+da6Kb1%g#5Gjbu-Hc;5z3=`*pL!+4}nSB z2QVhw4TRVgWe`C=r)PWaY9&>bbruKK>` z!j}oEh7rR4<0~cAEa2ZlD0|jl3%qmwqg6PSmPF(@O%AqJzxI>r$Z&!VC5Kz9;1^!8h#7+m#kX`KC zV5?)}&X||tveDD5JJdzo?48gy$D=i;udzF!Mq|qlGe@}IIR;$q*8q^3?F^4M_T^qo zPI(4%@}QB~13n|?y)WLnf9v!6SI-tM)W5^QQJe`q48WJ+-i>$e-x|Mv>%DI#-iOH_ zL1zdc=Kuc}U-v)-8pvkSuf_*c$@G9m;~0uZMpv}sSvnjaNNW8wb!s@3&FD%3Ygjx> z2l|rKh%j>T)$Ya6N8?(quevK9?}vJ>^(WKmcpSzz08EVlv>E64AVwnKWzlZ1zn7IA zU2vN%jd@w9-9yGYr^V=O%@+Q{{eat@K1Xg;GA&Re`2hQ-K==Yfp?U9V-dEovi&LPWOg8#)~FTjG6Gg< zip7~a5b*7%E`;*{x+#^V{9mEV?CZ*nqq^%*;@C(!K@aek*?6X9njA`L!%3>!yHTAN zQ{z7n{yy(THGC}ix#6Sv&O5HlEikT;baE%X2|PS*+6#brGa=+FA@Y?F1N@u7Ua4wr zXr3u=nk;X6D40nv$=d~!^^MA(R$lZ>k;p@nn@CrHux_$^9m`d%{lUmguzoUFzbJT# zv}hJg6?qf1@->Ah>l6t+H2H{hv3bV7cGACgQGgCI!+A948wyO@6e)XXasabuCQv&W zsAVmYs_uPITP{9Ig1x0I*vvm~DnwhRNa;h9AIO0jB|51@7X|1bH~0@U^Fs1p<1xdG diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 6b7e3dfe4b195dd1070f769c90a7f6b647fa035e..9f9dae20250bab779bca47ce52637511f90e0a25 100644 GIT binary patch delta 4500 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 E0H&b27ytkO delta 2401 zcma)8Z){Ul6z^-huIt*hUDtJOx2|31wl_8=Qnn$3Q6~dMMxYZC2)vZ`ZFF>Px$kvh zg4v8yL1K_x#~=R&B}T&-XbB%Qk@!VXKWSnlFD6RFf$@V;;fqG%IrnX|z-s97e!KUa zbMHI%cmCY>W%|ACWnWn=CJp?SuNqH$SADdsire`?qrmA$b-W{Yfjg(;cp5KS&$;ON zqOArOSQ2l}V)&fvsmF00fTG_Z%2Cn)Lk~QX1za>f!5$JOV8rb+Fr2lNwj7w4vbn``C- zKFhf!yteNzoxj!3>y+XQNyoD>nfPEIrASW5Ns;(rEO7vS<~Iq_+m^LlJ^i8FO@*>e zoq3*t5h+Dj1fUqxBrcLfYB1j|)G}8er#H$1HF;;O%s(TVEv60D;MiImwjr!T=tNi! zpp+zH>6DO)4vXYroSWD@F&rHfNf!!?Opql>vIG~cM_7<5RCLclEexd3046lQYqdr6 zvh`B~^Rhd4#CDUbX4L3oX*il6P48glt@3WhOhRk~-Gb-d0SbKCe8B54;B9vW+?kIJG_BxN(8)JxG3k=K&|?RGa9uyRO-?s1O}j69c0H=0XT}V9u~KWb9?=CXcgH0u2l2YBkZ(SrVlOJku%%(cFXH zAGJ`~cHbgBuO(r++Sl3Mk5&f&vSxrA({KO$(&Y6QFV2iV15RU;P>N%_(}|cU5gVFf zee`GPw9n@ZfL@`KmRL)8ZWNf#;mt|~-e&pv1r@|rnkKZ$vSFJS* z5wDa*`osNCMueVq8z1Xg6Hya(1NvQz&a!I?QCn7@@}k&Wh)8Y|iXXr$ix@9o{7p(n&7`8kg-vO98beHG|?pR4!cb z`_X)V&Rf^Zso~vEkJ#M0H#rS`x}u9-3ZAA*wH0<-+#WBVa-6}^gWt9?YF+AfRKyMN z=bSR0(a*}Q^s342LTTlcY2MnV)rk6`I=a{7Xf?!LQ&nfQv-ah2_k?DfL#tVs@FaM~Y*6}Jok*Qdv?eSDbo(7}dIuAg3R?6y^bm{cRE$w6IB zD;Kdo=?CE*y$R+|StBAD0csZuf+Q0`I7-KwoU`HoqN#1HU?+GTR=)zzpB4bTr2^Qr z7T?vhoD1bdUejWZv0kmm*37yz*#tYzcJ(K4xEeqyilvijLRO$)CBl4g3kP%(?GT=Y zNBW7Dv8cM82gI7#*Ghf3?`ro4@wXzvTN3la;FHF3hqhf&Vf<$ZX7iDK*;h7XCP vxQd*GDF=`A9Bt_s3Ocy9g9AsBhmtonFud7f<=S3095){|-_pR4eY*S$`LhqM diff --git a/backend/shop/__pycache__/models.cpython-313.pyc b/backend/shop/__pycache__/models.cpython-313.pyc index 6d22e886340afed237cdad8f13ef9f743e31b114..f0d45e9b8adbaf8ab8f3e6224877c1d585115eb1 100644 GIT binary patch delta 4621 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 X4DYPva=E&b^3$4An!6GhGME1Y1f9n9 delta 2625 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 delta 1590 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 delta 1925 zcma)-O>7%Q6vub`vGKQ^FUNMBWC@V60gNg)l_({O2!Tdwqd+K)sz{rackC=#e`MBf z8ZHi(Du_$NLqN}*nnO9{zyVZ5Ply9T;y_tIAa0xps(RYV? zv%dYxk7pA1MBo%?EPQd~lJy*|T-$L^9$I$vD*Y za46~yg>WdmH?;M+9FAZ=>h+%p$FLLMGX@itgqnm!mSZxkvFV-f*fwKnX!$uy!Y}@; zJcOwT1@f6&@L6D-4a3iYwLy0tJ37Ky=dZ+53wKF8W#=+KeSG$TYfLC6z2MOMgg3PDDj*10a-OATxW?{()ski8TX z94+|}51$}5{P^DgL4xnQ9%N5qTReks0+w0hHrW(K5yLybhNhT0f?d@wo^&0|QoSag z#-v3nuR`A-?@lVskb!U-Uz-Zzy7Z6F7Yyyn5?JXWPrDhok-o{MVQgS5>VyT?4Xs|Y z)?rtE5k4Hq&yxb;fAysDJC2@L(C-4UO#i{n+|E-mib=we`kR73GsX5C*`^7HWQ!l8 z%*5+7iLQ`1Lr@5hEUQaoCD2kf?b#CyKFSund@sSb*)P}vY~;qeRi7i<^_D1cv4F0x zso+bTg1fo0x`bUd=J-s*k@0Gk&59HCezlrjm;_-7xUJ*iK0BGM0hC)qt>YDN!ZV2{l3q@fKe0 z26a{mg5X_;NJrIVa@fBPPHMjlv|`!hmSH1@X65KJZ)cnM9sizPv5KWpo5#5S0xm{ z-KP`ZTc*Syj6>~4CTX=31%JSe(#tEIP){U0MC&VTiXi0k+vFm4y{U(k6E?xdfx(r__qwqi( I575H@0#n+0p8x;= diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index ad22381912f63bf68759fd33fe4acf54ddff5339..72b89fca87b0af3a298bbd54d6ce9f0d191ab689 100644 GIT binary patch delta 305 zcmeBSf5*;ynwOW00SKHO8#2Qt@=7v#O;op*$mNdWW@KPuNM%_Kk_3V%o>ZPJ>4{~E z%phtOgxaSV3gkf)Ad@K^sa&g>AR>$mAdSef5OxY@DmOw6&??3hE+EMSR4p@EfKgqX zJC!4aXASRaW{CQ9rYQbOK283~evG<|Y?Esk!+C{+Q;W(nlT-bRQc{a1pJr4OsNySz z2WHe+Hn5@hc!c@dFxrC`xP8MhyBM=w&0*MdIjEs!;8C35w7~E&5xz12C hS(@2V;06O*JAWhp1p&ni%4Sy>%qPb%>#%@)002F}JsSW3 delta 215 zcmaFI-ows&nwOW00SK5a8!|a3@=7u)O;ooQVPs%pNM%_Kk^q7z?o{q9$%$2p%phu> zqB@X*$RLv`?5UiqnIIyJ3}7A46!?JVFs5(N2uUUcwkYS%67yvK^BlBmd+ArjW@encC!JfNB|mxVRQbd|+l|WW3Ly adY8fAK12FdhV;p~%#M@yG3&5M0c8Q2U?^|^ diff --git a/backend/shop/__pycache__/urls.cpython-313.pyc b/backend/shop/__pycache__/urls.cpython-313.pyc index 6fa21a8f0ed696b3222cdddf9e3a62aff37ed2e3..d815fffbd3c802ea695f5aabb4276e0e2c1590ee 100644 GIT binary patch delta 299 zcmZo;f5FcCnU|M~0SKHO8#2Qt@=7wgOjNg)@Zv7wR$z!>QDQI!NdiF;PYh3x^u#hn zW)L+CLhVxw1@a&YkjY?<7%o#LhzQUa>}oh;xRKN_Dlr6e#V{#>RLe{jU{n|6j^POA z3FftA)@Lf>Pv_I*pX|Y?%g8plj4_;7I5@SaEHgRPzbGZOX!21;HGwL=Vu+A#K1i&1 z@@qyzMuEw)Od(7~Jd^X7I^|@6?qLMt;$9%}ftit!@jipJSfqfm0R1E-&Hw-a diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 4b4d42f1474fdce334ce9749dae7f22f8a4689f5..0ba8b2b070da3a0753e1f75afc1460ccdfa424a3 100644 GIT binary patch delta 1293 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 BMQ{KB delta 1070 zcmZ{j&rcIU6vubkZYk~dH~qCpTg#8K0VE&@1Omnw4sAf=8b!enEK4mGi_;Pm4@lC4 z12F-g8WV3`HEQU=7*EDOKoj(`hHw$Z8wnmfcyQh{qA{}B{p`Fq``&lv&D#f^-`X5| zcDq$z*U#?TnH|rXBS_lU;%Z}5P(`&O=}fq&E8(W@gok<(Uh0hsK_Mon7Fkd$$Ho6-5$(%a)EyWliZ|O+1UgG^1!b5eLdO<18WqdzNM^Uk940HMbA?E8md# zy7h>G_@Py*A#E@uDGuC|Zl20y3mT;nC`vJN17_)jr&5en?VS`Qt*|RCliJb^`zND( zjLpvBGIpLl-oA5x`{nwVd(S_=en`7YGwurF>cwaY!OPVLMbD%90kko%ZkWBQi#a-} zQQbbTrLIgA##6a#Q<*e94=26#Cx4SQ-cazCjjsW{N*FeRt6|-i*Jx%Un_19kzI@1Big%X> zRzmV3Z)?8YiXMAEbhirfw}ay|cfegRB@){X!G diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 50e70b7..99780ed 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.utils.html import format_html from django.db.models import Sum +from unfold.admin import ModelAdmin, TabularInline +from unfold.decorators import display from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature import qrcode from io import BytesIO @@ -11,13 +13,13 @@ admin.site.site_header = "量迹AI硬件销售管理后台" admin.site.site_title = "量迹AI后台" admin.site.index_title = "欢迎使用量迹AI管理系统" -class ProductFeatureInline(admin.TabularInline): +class ProductFeatureInline(TabularInline): model = ProductFeature extra = 1 fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order') @admin.register(WeChatPayConfig) -class WeChatPayConfigAdmin(admin.ModelAdmin): +class WeChatPayConfigAdmin(ModelAdmin): list_display = ('app_id', 'mch_id', 'is_active', 'notify_url') list_filter = ('is_active',) search_fields = ('app_id', 'mch_id') @@ -34,7 +36,7 @@ class WeChatPayConfigAdmin(admin.ModelAdmin): ) @admin.register(ESP32Config) -class ESP32ConfigAdmin(admin.ModelAdmin): +class ESP32ConfigAdmin(ModelAdmin): list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') list_filter = ('chip_type', 'has_camera') search_fields = ('name', 'description') @@ -53,13 +55,16 @@ class ESP32ConfigAdmin(admin.ModelAdmin): ) @admin.register(Service) -class ServiceAdmin(admin.ModelAdmin): +class ServiceAdmin(ModelAdmin): list_display = ('title', 'created_at') search_fields = ('title', 'description') fieldsets = ( ('基本信息', { 'fields': ('title', 'description', 'color') }), + ('价格与交付', { + 'fields': ('price', 'unit', 'delivery_time', 'delivery_content') + }), ('图标', { 'fields': ('icon', 'icon_url'), 'description': '图标上传和URL二选一,优先使用URL' @@ -74,7 +79,7 @@ class ServiceAdmin(admin.ModelAdmin): ) @admin.register(ARService) -class ARServiceAdmin(admin.ModelAdmin): +class ARServiceAdmin(ModelAdmin): list_display = ('title', 'created_at') search_fields = ('title', 'description') fieldsets = ( @@ -88,7 +93,7 @@ class ARServiceAdmin(admin.ModelAdmin): ) @admin.register(Salesperson) -class SalespersonAdmin(admin.ModelAdmin): +class SalespersonAdmin(ModelAdmin): list_display = ('name', 'code', 'total_sales', 'view_promotion_url') search_fields = ('name', 'code') readonly_fields = ('promotion_qr_code', 'promotion_url_display', 'total_sales_display') @@ -100,12 +105,11 @@ class SalespersonAdmin(admin.ModelAdmin): ) return queryset + @display(description="累计销售额 (已支付)", ordering='_total_sales') def total_sales(self, obj): # 仅计算已支付的订单 paid_sales = obj.orders.filter(status='paid').aggregate(total=Sum('total_price'))['total'] return f"¥{paid_sales or 0:.2f}" - total_sales.short_description = "累计销售额 (已支付)" - total_sales.admin_order_field = '_total_sales' def total_sales_display(self, obj): return self.total_sales(obj) @@ -116,15 +120,16 @@ class SalespersonAdmin(admin.ModelAdmin): base_url = "http://localhost:5173" return f"{base_url}/?ref={obj.code}" + @display(description="推广链接") def view_promotion_url(self, obj): url = self.promotion_url(obj) - return format_html('打开推广链接', url) - view_promotion_url.short_description = "推广链接" + return format_html('打开推广链接', url) def promotion_url_display(self, obj): return self.promotion_url(obj) promotion_url_display.short_description = "完整推广链接" + @display(description="推广二维码") def promotion_qr_code(self, obj): if not obj.code: return "请先保存以生成二维码" @@ -144,9 +149,7 @@ class SalespersonAdmin(admin.ModelAdmin): img.save(buffer, format="PNG") img_str = base64.b64encode(buffer.getvalue()).decode() - return format_html('', img_str) - - promotion_qr_code.short_description = "推广二维码" + return format_html('', img_str) fieldsets = ( ('基本信息', { @@ -161,7 +164,7 @@ class SalespersonAdmin(admin.ModelAdmin): ) @admin.register(Order) -class OrderAdmin(admin.ModelAdmin): +class OrderAdmin(ModelAdmin): list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at') list_filter = ('status', 'salesperson', 'created_at') search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no') diff --git a/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py b/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py new file mode 100644 index 0000000..bf9889a --- /dev/null +++ b/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 6.0.1 on 2026-02-02 06:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0007_productfeature'), + ] + + operations = [ + migrations.AddField( + model_name='service', + name='delivery_content', + field=models.TextField(blank=True, help_text='描述将交付给客户的具体成果', verbose_name='交付内容'), + ), + migrations.AddField( + model_name='service', + name='delivery_time', + field=models.CharField(blank=True, help_text='例如:3-5个工作日', max_length=50, verbose_name='预计交付周期'), + ), + migrations.AddField( + model_name='service', + name='price', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='起步价格'), + ), + migrations.AddField( + model_name='service', + name='unit', + field=models.CharField(default='次', help_text='例如:次、小时、月、个', max_length=20, verbose_name='计费单位'), + ), + migrations.CreateModel( + name='ServiceOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('customer_name', models.CharField(max_length=100, verbose_name='客户姓名')), + ('company_name', models.CharField(blank=True, max_length=100, verbose_name='公司名称')), + ('phone_number', models.CharField(max_length=20, verbose_name='联系电话')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='电子邮箱')), + ('requirements', models.TextField(blank=True, verbose_name='具体需求描述')), + ('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='预估总价')), + ('status', models.CharField(choices=[('pending', '待沟通/待支付'), ('processing', '服务进行中'), ('completed', '已完成'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='订单状态')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('salesperson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.salesperson', verbose_name='所属销售员')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.service', verbose_name='所选服务')), + ], + options={ + 'verbose_name': '服务订单', + 'verbose_name_plural': '服务订单列表', + }, + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index 017467f..b7395d0 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -137,6 +137,10 @@ class Service(models.Model): icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)") description = models.TextField(verbose_name="简介") features = models.TextField(verbose_name="特性列表", help_text="每行一个特性") + price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="起步价格") + unit = models.CharField(max_length=20, default="次", verbose_name="计费单位", help_text="例如:次、小时、月、个") + delivery_time = models.CharField(max_length=50, blank=True, verbose_name="预计交付周期", help_text="例如:3-5个工作日") + delivery_content = models.TextField(blank=True, verbose_name="交付内容", help_text="描述将交付给客户的具体成果") color = models.CharField(max_length=20, default="#00f0ff", verbose_name="主题色") detail_image = models.ImageField(upload_to='services/details/', blank=True, null=True, verbose_name="详情页长图 (上传)") detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)") @@ -150,6 +154,39 @@ class Service(models.Model): verbose_name_plural = "AI服务管理" +class ServiceOrder(models.Model): + """ + AI服务订单模型 + """ + STATUS_CHOICES = ( + ('pending', '待沟通/待支付'), + ('processing', '服务进行中'), + ('completed', '已完成'), + ('cancelled', '已取消'), + ) + + service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="所选服务") + customer_name = models.CharField(max_length=100, verbose_name="客户姓名") + company_name = models.CharField(max_length=100, blank=True, verbose_name="公司名称") + phone_number = models.CharField(max_length=20, verbose_name="联系电话") + email = models.EmailField(blank=True, verbose_name="电子邮箱") + requirements = models.TextField(verbose_name="具体需求描述", blank=True) + + total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="预估总价", default=0) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="订单状态") + + salesperson = models.ForeignKey(Salesperson, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属销售员") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return f"{self.customer_name} - {self.service.title}" + + class Meta: + verbose_name = "服务订单" + verbose_name_plural = "服务订单列表" + + class ARService(models.Model): """ AR体验服务模型 diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index ce5ae6d..e7b5635 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature +from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder class ProductFeatureSerializer(serializers.ModelSerializer): """ @@ -49,6 +49,38 @@ class ServiceSerializer(serializers.ModelSerializer): return obj.detail_image.url return None +class ServiceOrderSerializer(serializers.ModelSerializer): + """ + AI服务订单序列化器 + """ + service_name = serializers.CharField(source='service.title', read_only=True) + # 接收前端传来的 ref_code + ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True) + + class Meta: + model = ServiceOrder + fields = ['id', 'service', 'service_name', 'customer_name', 'company_name', + 'phone_number', 'email', 'requirements', 'total_price', 'status', 'created_at', 'ref_code'] + read_only_fields = ['total_price', 'status', 'created_at'] + + def create(self, validated_data): + ref_code = validated_data.pop('ref_code', None) + service = validated_data.get('service') + + # 默认设置预估总价为服务起步价 + if service: + validated_data['total_price'] = service.price + + # 尝试关联销售员 + if ref_code: + try: + salesperson = Salesperson.objects.get(code=ref_code) + validated_data['salesperson'] = salesperson + except Salesperson.DoesNotExist: + pass + + return super().create(validated_data) + class ARServiceSerializer(serializers.ModelSerializer): """ AR服务序列化器 diff --git a/backend/shop/urls.py b/backend/shop/urls.py index 40e6243..ebf41eb 100644 --- a/backend/shop/urls.py +++ b/backend/shop/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet +from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet router = DefaultRouter() router.register(r'configs', ESP32ConfigViewSet) router.register(r'orders', OrderViewSet) router.register(r'services', ServiceViewSet) router.register(r'ar', ARServiceViewSet) +router.register(r'service-orders', ServiceOrderViewSet) urlpatterns = [ path('', include(router.urls)), diff --git a/backend/shop/views.py b/backend/shop/views.py index 17e18f6..38b8606 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -2,8 +2,8 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from django.shortcuts import render -from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService -from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer +from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder +from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer class ARServiceViewSet(viewsets.ReadOnlyModelViewSet): """ @@ -28,6 +28,13 @@ class ServiceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Service.objects.all().order_by('-created_at') serializer_class = ServiceSerializer +class ServiceOrderViewSet(viewsets.ModelViewSet): + """ + AI服务订单管理 + """ + queryset = ServiceOrder.objects.all() + serializer_class = ServiceOrderSerializer + class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet): """ 提供ESP32配置选项的列表和详情 diff --git a/frontend/src/api.js b/frontend/src/api.js index 8eb932e..29ea1d0 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -16,6 +16,7 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_ export const getServices = () => api.get('/services/'); export const getServiceDetail = (id) => api.get(`/services/${id}/`); +export const createServiceOrder = (data) => api.post('/service-orders/', data); export const getARServices = () => api.get('/ar/'); export default api; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 3ae1e9c..71774e5 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -19,10 +19,9 @@ const Home = () => { fetchProducts(); let i = 0; const typingInterval = setInterval(() => { - if (i < fullText.length) { - setTypedText(prev => prev + fullText.charAt(i)); - i++; - } else { + i++; + setTypedText(fullText.slice(0, i)); + if (i >= fullText.length) { clearInterval(typingInterval); } }, 150); @@ -73,6 +72,26 @@ const Home = () => { return (
+ + + <span className="neon-text-green">{typedText}</span><span className="cursor-blink">|</span> diff --git a/frontend/src/pages/ServiceDetail.jsx b/frontend/src/pages/ServiceDetail.jsx index b393ccb..1630920 100644 --- a/frontend/src/pages/ServiceDetail.jsx +++ b/frontend/src/pages/ServiceDetail.jsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Typography, Button, Spin, Empty } from 'antd'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { getServiceDetail } from '../api'; +import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd'; +import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons'; +import { getServiceDetail, createServiceOrder } from '../api'; import { motion } from 'framer-motion'; const { Title, Paragraph } = Typography; @@ -12,6 +12,9 @@ const ServiceDetail = () => { const navigate = useNavigate(); const [service, setService] = useState(null); const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [form] = Form.useForm(); useEffect(() => { const fetchDetail = async () => { @@ -27,6 +30,28 @@ const ServiceDetail = () => { fetchDetail(); }, [id]); + const handlePurchase = async (values) => { + setSubmitting(true); + try { + const orderData = { + service: service.id, + customer_name: values.customer_name, + company_name: values.company_name, + phone_number: values.phone_number, + email: values.email, + requirements: values.requirements + }; + await createServiceOrder(orderData); + message.success('需求已提交,我们的销售顾问将尽快与您联系!'); + setIsModalOpen(false); + } catch (error) { + console.error(error); + message.error('提交失败,请重试'); + } finally { + setSubmitting(false); + } + }; + if (loading) { return (
@@ -62,36 +87,135 @@ const ServiceDetail = () => { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} > -
- - {service.title} - - - {service.description} - -
+ + +
+ + {service.title} + + + {service.description} + + +
+ 服务详情 + + 交付周期}> + {service.delivery_time || '待沟通'} + + 交付内容}> + {service.delivery_content || '根据需求定制'} + + +
+
- {service.display_detail_image ? ( -
- {service.title} -
- ) : ( -
- 暂无详情图片 -
- )} + {service.display_detail_image ? ( +
+ {service.title} +
+ ) : ( +
+ 暂无详情图片 +
+ )} + + + +
+
+ 服务报价 +
+ ¥{service.price} + / {service.unit} 起 +
+ +
+ {service.features_list && service.features_list.map((feat, i) => ( + + {feat} + + ))} +
+ + +

+ * 具体价格可能因需求复杂度而异,提交需求后我们将提供详细报价单 +

+
+
+ +
+ + {/* Purchase Modal */} + setIsModalOpen(false)} + footer={null} + destroyOnHidden + > +

请填写您的联系方式和需求,我们的技术顾问将在 24 小时内与您联系。

+
+ + + + + + + + + + + + + + + + +
+ + +
+
+
); };