From f1bc0a30e1306d239e6755c3c2cb422239f86662 Mon Sep 17 00:00:00 2001 From: nudoragon Date: Tue, 26 May 2026 12:04:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AA=BF=E6=95=B4=E5=84=80=E9=8C=B6=E6=9D=BF?= =?UTF-8?q?=E5=92=8C=E7=B7=A8=E8=BC=AF=E5=99=A8=E7=9A=84=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=BF=AB=E9=80=9F=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E6=8C=89=E9=88=95=EF=BC=8C=E4=B8=A6=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E7=9B=B8=E9=97=9C=E7=9A=84=E6=B8=AC=E8=A9=A6=E8=85=B3=E6=9C=AC?= =?UTF-8?q?=E5=92=8C=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/main.cpython-313.pyc | Bin 28936 -> 30247 bytes main.py | 25 +++ scripts/test_phase1.py | 63 ++++++ templates/dashboard.html | 133 ------------ templates/editor.html | 2 +- templates/project_dashboard.html | 203 ++++++++++++++++++ websites/phase1-e2e/index.html | 33 +++ ...3179739a9b94477881912423503cd09c_thumb.png | Bin 0 -> 291 bytes websites/phase1-e2e/media/images/uploads.json | 10 + websites/phase1-e2e/project.json | 11 + 10 files changed, 346 insertions(+), 134 deletions(-) create mode 100644 scripts/test_phase1.py create mode 100644 templates/project_dashboard.html create mode 100644 websites/phase1-e2e/index.html create mode 100644 websites/phase1-e2e/media/images/2026-05/3179739a9b94477881912423503cd09c_thumb.png create mode 100644 websites/phase1-e2e/media/images/uploads.json create mode 100644 websites/phase1-e2e/project.json diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index 1585d1426ef45be47d9ffa6304c179db16c366c5..44e028f895632e88eb28ce282a484559576555cd 100644 GIT binary patch delta 2697 zcmZ`*dr(x@8NcW519q>=@><@ST?Cc|Gxe;(wQ*u zyT9*zuk)SropU($Px9IqBx=rNG79kXZL-B*yz5j{5*a&Xe>k{}Mx_XTEn7>sQ?tr4 zXf2IaSthU;m1WT#G*)GHT1VqlmQCwvyviJ)wWutIevdZL1mu3ga%m$?RLApZ6HQXt zI{JOuOp_NZ^7(IwjkNGT5GUErzXqf{N6cR3A~;n+pp@EpyO=4Z)3q|886_fZ;m?V7 zDQih?<=0d>dqLmIzXCa(<}ApM^9+sMl(%e8*YVw&Oev2pdmPi)L6(;yyr*#(7W1|8 zFIGrvwD#1ZWnb#TQe5-X6<8S$iHW8SOW9j#F(1}uMr@qV&&Rau42yYo@@a_WT(aFo zH?N3QiLp}4mh`)6`3ij%>T@fW*6InmWre8NBNPi*J?2Jvp&DKBN*VbnQ~J+R5v92TQ96KpBOwd zIBAwArGk*QfUO0^;bghf6FB%}pNDnI3vTk}z@dSz@{WaF1S>&^fIp2Okh!07B31I$ zzKp>(gQ@|b;^P9TD z6Vlcw8?qWj4Z9%lk?d#5B_8W|k-W*zJDPRxfd3zrZyg_tx@$n^lu#ZM$w$0eF4p}S z2EI`S?8iFGXd5#W5HqCoK#-Vmcd!7!x=i3rzuy~}00f^-u?3BqN7nLhzIqL>K>Ztz!K_Oz== za}eqfjw18|I3n0he%IBk`x^*pO6i74B0Vx?9<$BD*KN!)eh%u5IGr+>j#GJV<1Dd0 zVh7q*wmdy`IwAh6(iC!ozgucEoYycJY#`U2(j4PmpcNSG_gEYE6+WgM*i=WNR|aGg z{MzPP-F+}_R7_>Dg#1IDj;lOV_b(XSrMSzZh<@IQ2P#^1U&3fR|G1)wUr$XSwfvOD z#kcI}7u#0xPb!o62gNaQ`U0}paWrDRm?5;8Pn+VkyJKl2UsIY)47|Zl?<*tIFTOl| z_R7?i=h-dLh7C)>SO{c?$R^;!W|12oIT#X-&hI|3u+bOr^#+2T-VSeA+v)2FvT8I~ zZihP{1~dRx_2}e+Ppqfk=f;l2lmWE>HU}05aNkCO%wG>lyt1&o$nrwxXw6?G+k|Ah zZB9575wdLxC2pQHmyHKP=BiL+)om&1O#Gy@W~k;?QrfZ939W^{QJ7+Eoro)(l!`*y zA|+wVsJOV7<5gMY7XN!yO421rbXfmmyjy+})&&m^^gP*$Rr8qY^*Z(dw7j{xLHZiV z08T7|@DsI8C0s3vu9b_-eh4;Dy(j2p7cpv-+~e)^d1PM?7AUhu7-W~A!y1hL0s1V4 zJHn6G7Ae2jwr{D;9&XRm<5s{F3TpGn1RUH9-9s37NjX}(M~qpS6n2(BsLO?7R@Y~z zeFkRVwN2bppFwW&qxD;LddPfE3DuXySkx0GVmkHoES&oEeVOxf_>ed3ds|Pyo_)$A z+nbB9u$jG>CL$aW80hHm1_Ek}gd=)@LZQrv)+aHfdXZe>C)(4=Wq!GRrxb-MwV}*B z+AYTlDC0}OEErM;H8;EC;1cw@cS7Y?+^GpSQ2P}?SPQpW4|@sa2mDKSO|*LBJdN@& zf`M1WIcpPP%O-)c~EL*C1ZEx zkg5g$tUEJw3u%0E5S)`kxidk5hf0zN6~qrmW{W}hSAJ<9NYWOyGk zy#CZP%J4ie>|%zk$*`jsE+oSxWLNn!J?X}3pzw50bE1xHC8*AxRq1TBGTQ4Wsd-}qV_ieZAyPgkN@P7+RLmlqO;i5cCz1aF DPN&L) delta 1979 zcmZ`(du)?c6z~1|7__V#Y+czv*Rgfox0RJLM%~g5Dt+T|L_k@2son27fc+9b{I`ejH_FO*bwC*kO>JW7=`#c=j#~b55Kf0zjMy- zJnp^c-eWh|zDvw}J2TU$!Ec|{(p|z2nP;+9haCUxT*l1>nr@va=3btqq7uHGXRFAL zXu66@`4fDGiX40e&rwksZ{)culF@5XQ8|B-H}O2e-Aq*QmE5Y@oqQFasiL{u$5-?G zaYiM4z=~Kie8t?%3xA?)fpV#A8ZUA&KBq?F&Ab>kN_JDpgvAdBBnRrH;|xC}P`B~2 zaeWQ^hI;uVeu>tRS^j{}ozSMWBV+DFhJ9Lx$u%irP%BTp-+>=)pmsmIA0E{?W>r7n z@`bdNYT8-+L_H)WYo=!+H|LMR9-TeImF^Ed*3F&B^%PvyIV$R^Svr=l<@FEcYG|L$ z7uB|DGy$7N(_XY@FR6r)g|5-8d7~zGQs-9gI#x^Cu_Fwc%HL!I@O}B)>?7D# z;WwSafY0zBb1>NGn++En8OrsF_a!|U#}ud(5<3G1x5xN38g3EuFxYC{$_wrh=1!n4 zhDe-1(R!Sv^qV6h7#3$xO&R;5p+Gzw+7yV(Qc8+!6=&eWvV1sFRf9QQKC3m_7vB^M z$KzeSoiQH6mt|eh5kpQ!&x@eH#<0NWGoJ283-N|N=NrohZ zJPvjm`aK0~1de(9`txX>q5SO`WrhSXhTv??68%pIE0tNbrIIaeC6~Go@u03+=UWKW z6|f|{R99q{wZcWj5fV$G$eWj+UgxH$h;)a8ad8nnbk?yiX(8ZTP^ifDO)NV>5kn-X zh!}?5iyHM;5PFs5q8W_+2$vVv8n2@d6-+t%0la!McH($~|`DofbF zFDn*IOBuUjUA?h*pf?zX;zpn8S7f(S0~s_DZe^fRlByGEoYedCsHSvuy5d8GJIt=0 zaHuQb>gow}hGQy} zM$7GNi+Yw4Wl_(Kwg?C5PQae7Jp_dvrwsSVdJa+@{wi9~SUUB3kJinm>5Pq zmC=I`7bs{<#eND=XLA5H2IsJo@LF)KDT7?Z7E|C-&{cYuU@i#@3R$1_sbOk@gX9_j zPiQT!|3GM#HHik?+OA+crNhhABaRZ|CVU?9W}hawo5UUx66A!}+0;SI!m=s0HN~Dw zuaZ$4YHls`hu2GgX<#h8l$pS}dA`0Fo$n~Y%~Pl7?Fiklz1Pb=g-?2`tTM$lV{%!S zPDEJ9ha(myLteygC?|pp4Us%Fv_%%8JQ#5_sc}y9VccE{F{p7wt{^KL(^68yq@?c> zF-t(=BLZ1&yDe uofWjF;xs%f<{0Y{;!hK6hJJBAi?R8)wHhh^mW1fm6y<2Fn<@4_iTwv5f(G^g diff --git a/main.py b/main.py index f4d1383..450c178 100644 --- a/main.py +++ b/main.py @@ -184,9 +184,19 @@ def index() -> Response: @app.route("/dashboard") # type: ignore[untyped-decorator] def dashboard() -> str: + # render projects overview return str(render_template("dashboard.html")) +@app.route("/dashboard/project/") # type: ignore[untyped-decorator] +def project_dashboard(slug: str) -> str: + # render single project management dashboard + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + abort(404) + return str(render_template("project_dashboard.html", slug=slug)) + + @app.route("/editor/") # type: ignore[untyped-decorator] def editor(slug: str) -> str: proj_dir = _project_dir(slug) @@ -345,6 +355,21 @@ def api_pages_tree(slug: str) -> tuple[Response, int] | Response: return jsonify(tree) +@app.route("/api/projects//pages-tree", methods=["PUT"]) # type: ignore[untyped-decorator] +def api_put_pages_tree(slug: str) -> tuple[Response, int] | Response: + """儲存自訂的頁面樹狀結構到 project.json 的 page_tree 欄位。""" + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + body: dict[str, Any] = request.get_json(force=True) or {} + if not isinstance(body, dict): + return jsonify({"error": "不正確的資料格式"}), 400 + data = _load_project(slug) + data["page_tree"] = body + data["updated_at"] = _now_iso() + _save_project(slug, data) + return jsonify({"ok": True, "page_tree": body}) + + @app.route("/api/projects//media/upload", methods=["POST"]) # type: ignore[untyped-decorator] def api_media_upload(slug: str) -> tuple[Response, int] | Response: proj_dir = _project_dir(slug) diff --git a/scripts/test_phase1.py b/scripts/test_phase1.py new file mode 100644 index 0000000..a1dde66 --- /dev/null +++ b/scripts/test_phase1.py @@ -0,0 +1,63 @@ +"""Automated basic end-to-end test for Phase 1 features. +Creates a project, updates settings, uploads an image, lists media, deletes image. +""" +import io +import os +import sys +import time +import json +import requests +from PIL import Image + +BASE = 'http://127.0.0.1:5000' + +def create_project(name='test-project'): + r = requests.post(f'{BASE}/api/projects', json={'name': name, 'description': 'automated test'}) + r.raise_for_status() + return r.json() + +def update_settings(slug): + r = requests.put(f'{BASE}/api/projects/{slug}/settings', json={'title':'Auto Test','description':'desc'}) + r.raise_for_status() + return r.json() + +def upload_image(slug): + # generate small PNG + im = Image.new('RGB', (100,100), color=(123,222,100)) + buf = io.BytesIO() + im.save(buf, format='PNG') + buf.seek(0) + files = {'file': ('test.png', buf, 'image/png')} + r = requests.post(f'{BASE}/api/projects/{slug}/media/upload', files=files) + r.raise_for_status() + return r.json() + +def list_media(slug): + r = requests.get(f'{BASE}/api/projects/{slug}/media/list') + r.raise_for_status() + return r.json() + +def delete_media(slug, rel): + r = requests.delete(f'{BASE}/api/projects/{slug}/media/{rel}') + return r + +if __name__ == '__main__': + print('Creating project...') + proj = create_project('phase1-e2e') + slug = proj['slug'] + print('Project created:', slug) + print('Updating settings...') + print(update_settings(slug)) + print('Uploading image...') + up = upload_image(slug) + print('Upload result:', up) + time.sleep(0.5) + items = list_media(slug) + print('Media list count:', len(items)) + if items: + it = items[0] + rel = f"media/images/{it['date']}/{it['filename']}" + print('Deleting', rel) + r = delete_media(slug, rel) + print('Delete status', r.status_code) + print('Done') diff --git a/templates/dashboard.html b/templates/dashboard.html index 8bdc7df..acc3d10 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,136 +1,3 @@ - - - - - - 管理器 - Dashboard - - - - -
-

網站管理器

-
- -
-
概覽
-
網站設定
-
頁面管理
-
媒體庫
-
- -
-
選擇一個專案在右側編輯。
- - - - - - -
- - - - diff --git a/templates/editor.html b/templates/editor.html index 742f65a..2db982f 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -2319,7 +2319,7 @@ window.VVVEB_PROJECT_SLUG = "{% endraw %}{{ slug | safe }}{% raw %}"; if(!btn) return; btn.addEventListener('click', function(e){ var slug = window.VVVEB_PROJECT_SLUG || ''; - var url = '/dashboard' + (slug ? ('?slug='+encodeURIComponent(slug)) : ''); + var url = '/dashboard/project/' + encodeURIComponent(slug || ''); window.open(url, '_blank'); }); }); diff --git a/templates/project_dashboard.html b/templates/project_dashboard.html new file mode 100644 index 0000000..c4ec8d9 --- /dev/null +++ b/templates/project_dashboard.html @@ -0,0 +1,203 @@ + + + + + + 專案管理 + + + +
+

專案管理

+
+ +
+
概覽
+
網站設定
+
頁面管理
+
媒體庫
+
+ +
+
專案:
+ + + + + + +
+ + + + diff --git a/websites/phase1-e2e/index.html b/websites/phase1-e2e/index.html new file mode 100644 index 0000000..8821b1c --- /dev/null +++ b/websites/phase1-e2e/index.html @@ -0,0 +1,33 @@ + + + + + + + + My page + + + + + + + + + +
+
+
+

Bootstrap 5 start page

+

Start by dragging components to page or double click to edit text

+
+
+
+ + diff --git a/websites/phase1-e2e/media/images/2026-05/3179739a9b94477881912423503cd09c_thumb.png b/websites/phase1-e2e/media/images/2026-05/3179739a9b94477881912423503cd09c_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b37ef50bf120cee76b77162667ae7fe596fefd GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^DIm