feat(creator-studio): icon toolbar with block tools, drop author strip

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 22:30:08 +08:00
1 parent 7bdf7cf018
commit 7c33482be7
19 files changed
+600 -241

No files matched your search

+1 -1
View File
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'ccaa65aec8e25f38d35c');
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '1f4dc4e2d488af6bd803');
+1 -1
View File
@@ -1 +1 @@
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:60px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;height:34px;justify-content:center;min-width:32px;padding:0 8px}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#edf3f0;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-labeled span{white-space:nowrap}.jio-creator__format-select{background:transparent;border:1px solid transparent;border-radius:7px;color:var(--jc-ink-soft);flex:0 0 auto;font:inherit;font-size:13px;height:34px;padding:0 8px;width:94px}.jio-creator__format-select:focus-visible,.jio-creator__format-select:hover{background:#fff;border-color:var(--jc-line);outline:0}.jio-creator__format-select:disabled{color:#aebbb5}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:var(--jc-warn)}.jio-creator__btn,.jio-creator__ghost,.jio-creator__primary{align-items:center;border:1px solid transparent;border-radius:7px;cursor:pointer;display:inline-flex;font-size:13px;justify-content:center;min-height:34px;padding:0 13px;transition:background .18s ease,border-color .18s ease,color .18s ease;white-space:nowrap}.jio-creator__ghost{background:transparent;border-color:var(--jc-line)}.jio-creator__ghost:focus-visible,.jio-creator__ghost:hover{border-color:var(--jc-accent);color:var(--jc-accent)}.jio-creator__primary{background:var(--jc-accent);color:#fff}.jio-creator__primary:focus-visible,.jio-creator__primary:hover{backLine truncated
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:64px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;width:auto}.jio-creator__tool-btn i{font-size:14px;line-height:1}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#f0f2f1;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-label{max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__format-field{align-items:center;border-radius:6px;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;gap:2px;height:46px;justify-content:center;min-width:48px;padding:5px 6px;position:relative;width:auto}.jio-creator__format-field i{font-size:14px;line-height:1}.jio-creator__format-field:focus-within,.jio-creator__format-field:hover{background:#f0f2f1;color:var(--jc-accent-strong)}.jio-creator__format-field:focus-within{outline:2px solid rgba(38,118,101,.35);outline-offset:-2px}.jio-creator__format-select{background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;height:100%;inset:0;opacity:0;padding:0;position:absolute;width:100%}.jio-creator__format-select:disabled{cursor:default}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:var(--jc-warn)}.jio-creator__btn,.jio-creator__ghost,.jio-creator__primary{align-items:center;border:1px solid transparent;border-radius:7px;cursor:poLine truncated
+3 -3
View File
@@ -1,3 +1,3 @@
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function r(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),r=new Error(String(a));return r.status=t,r.code=e&&e.code?String(e.code):"",r.data=e,r}async function n(e){try{return await e.json()}catch(e){return null}}async function o(e,t,a){const o=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await n(o);if(!o.ok)throw r(i,o.status);return i}async function i(e,t,a,o={}){const i=new FormData;i.append("file",t),Object.entries(o).forEach(([e,t])=>i.append(e,t));const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:i}),c=await n(l);if(!l.ok)throw r(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(r){const{draft:n,setDraft:o,capabilities:s,limits:u,taxonomy:m,isPublishedEdit:d,onClose:p,onSaveDraft:_,onSubmit:b,privateStorage:f,privateUploads:g,saving:j,captcha:h}=r,[E,y]=(0,t.useState)(""),[v,N]=(0,t.useState)(""),[w,k]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(n.title||"").trim().length,[n.title])>=Number(u.titleMin||1),I=()=>{const e=E.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);o({tags:Array.from(new Set([...n.tags,...t]))}),y("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},d?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},d&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===m.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),m.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(n.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:n.categories.includes(t.id),onChange:()=>(e=>{const t=n.categories.includes(e);o({categories:t?n.categories.filter(t=>t!==e):[...n.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),m.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},n.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>o({tags:n.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:E,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>y(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),I())}}),(0,e.createElement)("button",{type:"button",onClick:I},(0,a.__)("添加","jio")))),m.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const r=n.custom[t.key]||[],i=r.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(i?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:i,onChange:()=>o({custom:{...n.custom,[t.key]:i?r.filter(e=>e!==a.key):[...r,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}function f({author:t}){const r="string"==typeof t?.name?t.name.trim():"";if(""===r)return null;const n="string"==typeof t?.avatar?t.avatar:"",o="string"==typeof t?.meta?t.meta:"";return(0,e.createElement)("div",{className:"jio-creator__author"},""!==n?(0,e.createElement)("img",{className:"jio-creator__author-avatar",src:n,alt:"",width:24,height:24}):(0,e.createElement)("span",{className:"jio-creator__author-avatar is-placeholder","aria-hidden":"true"},r.slice(0,1)),(0,e.createElement)("span",{className:"jio-creator__author-name"},r),""!==o&&(0,e.createElement)("span",{className:"jio-creator__author-meta"},o),(0,e.createElement)("span",{className:"jio-creator__author-hint"},(0,a.__)("原创内容","jio")))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function j({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function h(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const E=window.jioCreatorStudio||{},y=E.nonce||"",v=`jio-studio-${E.sourceId||0}`,N="jio-creator-content";function w(){const e=E.pay||{},t=E.categories&&E.categories.selected||[],a=E.tags&&E.tags.items||[];return{postId:Number(E.sourceId||0),versionId:Number(E.versionId||0),rev:Number(E.versionRev||0),title:String(E.title||""),content:String(E.content||""),schedule:{enabled:!1,at:""},categories:t.map(Number),tags:a.slice(),custom:E.custom||{},featured:E.featured||{type:"",data:{}},pay:{enabled:!!e.enabled,modo:"points"===e.payModo?"points":"cash",price:String(e.price||"0.00"),points:Number(e.points||0),vip1Price:String(e.vip1Price||"0.00"),vip2Price:String(e.vip2Price||"0.00"),payLimit:Number(e.payLimit||0),downloads:Array.isArray(e.downloads)?e.downloads:[]}}}function k(e){return e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function C(e){const t=String(e||"").trim();if(!t)return"";try{const e=new URL(t,window.location.href);return"http:"===e.protocol||"https:"===e.protocol?e.href:""}catch(e){return""}}function S(e){const t=document.createElement("div");return t.innerHTML=e,(t.textContent||"").replace(/\s+/g,"")}function x(){const[r,n]=(0,t.useState)(w),i=(0,t.useRef)(r);i.current=r;const l=(0,t.useRef)(null),c=(0,t.useRef)(null),m=(0,t.useRef)(null),d=(0,t.useRef)(!1),p=(0,t.useRef)(()=>{}),[_,g]=(0,t.useState)(!1),[x,I]=(0,t.useState)({canUndo:!1,canRedo:!1}),[M,L]=(0,t.useState)("p"),[R,D]=(0,t.useState)(()=>({state:E.sourceId||E.versionId||E.content?"saved":"ready",at:""})),[P,U]=(0,t.useState)(!1),[$,F]=(0,t.useState)(null),[T,z]=(0,t.useState)(null),K=(0,t.useRef)(!1),O=(0,t.useRef)(0),A=E.capabilities||{},B={titleMin:Number(E.limits?.titleMin||1),titleMax:Number(E.limits?.titleMax||30),titlePlaceholder:String(E.limits?.titlePlaceholder||(0,a.__)("请输入标题","jio")),priceMin:Number(E.pay?.priceMin||0),priceMax:Number(E.pay?.priceMax||0),pointsMin:Number(E.pay?.pointsMin||0),pointsMax:Number(E.pay?.pointsMax||0)},J=!!E.isPublished,q=(0,t.useCallback)(e=>{K.current=!0,O.current+=1;const t={...i.current,...e};i.current=t,n(t)},[]),V=(0,t.useCallback)(e=>{if(e===i.current.content)return;K.current=!0,O.current+=1;const t={...i.current,content:e};i.current=t,n(t)},[]),H=(0,t.useCallback)(e=>{const t=e?.undoManager;t&&I({canUndo:"function"==typeof t.hasUndo&&t.hasUndo(),canRedo:"function"==typeof t.hasRedo&&t.hasRedo()})},[]),W=(0,t.useCallback)(e=>{const t=e?.selection?.getNode(),a=t?e.dom.getParent(t,"p,h1,h2,h3,blockquote")||t:null,r=a?.nodeName?.toLowerCase()||"p";L(["h1","h2","h3","blockquote"].includes(r)?r:"p")},[]),X=(0,t.useMemo)(()=>Array.from(S(r.content)).length,[r.content]),Y=(0,t.useCallback)((e={})=>{const t=i.currenLine truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(B.titleMax)||100),spellCheck:!1,maxLength:Number(B.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:N,defaultValue:r.content,onChange:e=>V(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:r.featured,canEditCover:!!A.canUploadImage,canSchedule:!!A.canSchedule,scheduleEnabled:!!r.schedule.enabled,scheduleAt:r.schedule.at,onFeaturedChange:e=>q({featured:e}),onScheduleChange:(e,t)=>q({schedule:{enabled:e,at:t}})}),(0,e.createElement)("footer",{className:"jio-creator__footer"},(0,e.createElement)("span",null,(0,a.__)("字数","jio"),":",X),(0,e.createElement)("span",null,(0,a.__)("约","jio")," ",Math.max(1,Math.round(X/400))," ",(0,a.__)("分钟阅读","jio"))),P&&(0,e.createElement)(s,{draft:r,setDraft:q,capabilities:A,limits:B,taxonomy:{categories:E.categories&&E.categories.items||[],tags:!!E.tags?.enabled,customDefs:E.customDefs||[]},isPublishedEdit:J,onClose:()=>U(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{G(e)},privateStorage:!!E.privateStorage,privateUploads:E.privateUploads||[],captcha:!!E.captcha?.enabled,saving:"saving"===R.state||"submitting"===R.state}))}function I(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(x,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()})();
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function r(e){try{return await e.json()}catch(e){return null}}async function o(e,t,a){const o=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await r(o);if(!o.ok)throw n(i,o.status);return i}async function i(e,t,a,o={}){const i=new FormData;i.append("file",t),Object.entries(o).forEach(([e,t])=>i.append(e,t));const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:i}),c=await r(l);if(!l.ok)throw n(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(n){const{draft:r,setDraft:o,capabilities:s,limits:u,taxonomy:d,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:y,captcha:j}=n,[E,h]=(0,t.useState)(""),[v,N]=(0,t.useState)(""),[w,k]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const I=(0,t.useMemo)(()=>(r.title||"").trim().length,[r.title])>=Number(u.titleMin||1),x=()=>{const e=E.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);o({tags:Array.from(new Set([...r.tags,...t]))}),h("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===d.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),d.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(r.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:r.categories.includes(t.id),onChange:()=>(e=>{const t=r.categories.includes(e);o({categories:t?r.categories.filter(t=>t!==e):[...r.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),d.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},r.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>o({tags:r.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:E,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>h(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),x())}}),(0,e.createElement)("button",{type:"button",onClick:x},(0,a.__)("添加","jio")))),d.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=r.custom[t.key]||[],i=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(i?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:i,onChange:()=>o({custom:{...r.custom,[t.key]:i?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function y({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function j(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const E=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function h(e){const t=e?"ol":"ul";return`<${t}><li></li></${t}><p></p>`}function v(){const e=(e,t)=>`<${e}>${E(t)}</${e}>`;return`<table class="jio-content-table"><thead><tr>${[1,2,3].map(t=>e("th",`${(0,a.__)("列","jio")} ${t}`)).join("")}</tr></thead><tbody>${[1,2].map(()=>`<tr>${[1,2,3].map(()=>e("td","")).join("")}</tr>`).join("")}</tbody></table><p></p>`}const N=window.jioCreatorStudio||{},w=N.nonce||"",k=`jio-studio-${N.sourceId||0}`,C="jio-creator-content";function S(){const e=N.pay||{},t=N.categories&&N.categories.selected||[],a=N.tags&&N.tags.items||[];return{postId:Number(N.sourceId||0),versionId:Number(N.versionId||0),rev:Number(N.versionRev||0),title:String(N.title||""),content:String(N.content||""),schedule:{enabled:!1,at:""},categories:t.map(Number),tags:a.slice(),custom:N.custom||{},featured:N.featured||{type:"",data:{}},pay:{enabled:!!e.enabled,modo:"points"===e.payModo?"points":"cash",price:String(e.price||"0.00"),points:Number(e.points||0),vip1Price:String(e.vip1Price||"0.00"),vip2Price:String(e.vip2Price||"0.00"),payLimit:Number(e.payLimit||0),downloads:Array.isArray(e.downloads)?e.downloads:[]}}}function I(e){return e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function x(e){const t=String(e||"").trim();if(!t)return"";try{const e=new URL(t,window.location.href);return"http:"===e.protocol||"https:"===e.protocol?e.href:""}catch(e){return""}}function M(){const[n,r]=(0,t.useState)(S),i=(0,t.useRef)(n);i.current=n;const l=(0,t.useRef)(null),c=(0,t.useRef)(null),u=(0,t.useRef)(null),m=(0,t.useRef)(!1),p=(0,t.useRef)(()=>{}),[_,f]=(0,t.useState)(!1),[g,M]=(0,t.useState)({canUndo:!1,canRedo:!1}),[L,R]=(0,t.useState)("p"),[$,D]=(0,t.useState)(()=>({state:N.sourceId||N.versionId||N.content?"saved":"ready",at:""})),[P,U]=(0,t.useState)(!1),[F,T]=(0,t.useState)(null),[z,K]=(0,t.useState)(null),O=(0,t.useRef)(!1),B=(0,t.useRef)(0),A=N.capabilities||{},q={titleMin:Number(N.limits?.titleMin||1),titleMax:Number(N.limits?.titleMax||30),titlePlaceholder:String(N.limits?.titlePlaceholder||(0,a.__)("请输入标题","jio")),priceMin:Number(N.pay?.priceMin||0),priceMax:Number(N.pay?.priceMax||0),pointsMin:Number(N.pay?.pointsMin||0),pointsMax:Number(N.pay?.pointsMax||0)},J=!!N.isPublished,V=(0,t.useCallback)(e=>{O.current=!0,B.current+=1;const t={...i.current,...e};i.current=t,r(t)},[]),H=(0,t.useCallback)(e=>{if(e===i.current.content)return;O.current=!0,B.current+=1;const t={...i.current,content:e};i.current=t,r(t)},[]),W=(0,t.useCallback)(e=>{const t=e?.undoManager;t&&M({canUndo:"function"==typeof t.hasUndo&&t.hasUndo(),canRedo:"function"==typeof t.hasRedo&&t.hasRedo()})},[]),X=(0,t.useCallback)(e=>{const t=e?.selection?.getNode(),a=t?e.dom.getParent(t,"p,h1,h2,h3,blockquote")||t:null,n=a?.nodeName?.toLowerCase()||"p";R(["h1","h2","h3","blockquote"].includes(n)?n:"p")},[]),Y=(0,t.useCallback)((e={})=>{const t=i.current;return{post_id:t.postId,version_id:t.versionId,rev:t.rev,title:t.title,content:t.content,categories:t.categories,tags:t.tags,custom:t.custom,featured:t.featured,schedule:{enabled:!!t.schedule.enabled,at:t.schedule.enabled?t.schedule.at:""},pay:j(t.pay),...e}},[]),Z=(0,t.useCallback)(async(e=!1)=>{const t=l.current;t&&!t.removed&&H(t.getContent());const a=i.current;if(!O.current&&(a.postId>0||a.versionId>0))return!0;const n=Line truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(q.titleMax)||100),spellCheck:!1,maxLength:Number(q.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:C,defaultValue:n.content,onChange:e=>H(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!A.canUploadImage,canSchedule:!!A.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>V({featured:e}),onScheduleChange:(e,t)=>V({schedule:{enabled:e,at:t}})}),P&&(0,e.createElement)(s,{draft:n,setDraft:V,capabilities:A,limits:q,taxonomy:{categories:N.categories&&N.categories.items||[],tags:!!N.tags?.enabled,customDefs:N.customDefs||[]},isPublishedEdit:J,onClose:()=>U(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{G(e)},privateStorage:!!N.privateStorage,privateUploads:N.privateUploads||[],captcha:!!N.captcha?.enabled,saving:"saving"===$.state||"submitting"===$.state}))}function L(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(M,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",L):L()})();
@@ -68,7 +68,6 @@ const mockBootstrap = {
isLoggedIn: true,
},
limits: { titleMin: 5, titleMax: 100, titlePlaceholder: '请输入标题', contentPlaceholder: '请输入正文' },
author: { name: '预览作者', avatar: '', meta: '' },
privateUploads: [],
privateStorage: false,
media: { maxBytes: 5242880, batch: 6 },
@@ -1,40 +0,0 @@
/**
* 作者行:标题下方的头像 + 昵称(公众号式)。
*
* 数据来自服务端 bootstrap,前端只负责展示;没有作者信息时整行不渲染,
* 避免出现空头像占位。
*
* @package Jio
*/
import { __ } from '@wordpress/i18n';
type Author = {
name?: string;
avatar?: string;
meta?: string;
};
export function AuthorRow({ author }: { author: Author }) {
const name = typeof author?.name === 'string' ? author.name.trim() : '';
if (name === '') {
return null;
}
const avatar = typeof author?.avatar === 'string' ? author.avatar : '';
const meta = typeof author?.meta === 'string' ? author.meta : '';
return (
<div className="jio-creator__author">
{avatar !== '' ? (
<img className="jio-creator__author-avatar" src={avatar} alt="" width={24} height={24} />
) : (
<span className="jio-creator__author-avatar is-placeholder" aria-hidden="true">
{name.slice(0, 1)}
</span>
)}
<span className="jio-creator__author-name">{name}</span>
{meta !== '' && <span className="jio-creator__author-meta">{meta}</span>}
<span className="jio-creator__author-hint">{__('原创内容', 'jio')}</span>
</div>
);
}
@@ -1,3 +1,12 @@
/**
* 固定顶栏里的正文工具条。
*
* 每个按钮都是「上图标 + 下文字」,与公众号/知乎的单行工具栏一致;
* 只渲染当前账号真正有权限使用的能力,全部动作由父组件转给 TinyMCE。
*
* @package Jio
*/
import { __ } from '@wordpress/i18n';
type Capabilities = Record<string, boolean>;
@@ -12,9 +21,34 @@ type Props = {
onCommand: (name: string, value?: string) => void;
onRememberSelection: () => void;
onOpenLink: () => void;
onOpenMedia: (type: 'image' | 'video') => void;
onOpenMedia: (type: 'image' | 'video' | 'file') => void;
onInsertBlock: (kind: string) => void;
};
type ToolButtonProps = {
label: string;
icon: string;
ready: boolean;
onClick: () => void;
title?: string;
};
function ToolButton({ label, icon, ready, onClick, title }: ToolButtonProps) {
return (
<button
type="button"
className="jio-creator__tool-btn"
disabled={!ready}
onClick={onClick}
title={title || label}
aria-label={title || label}
>
<i className={icon} aria-hidden="true" />
<span className="jio-creator__tool-label">{label}</span>
</button>
);
}
export function EditorToolbar({
ready,
canUndo,
@@ -26,6 +60,7 @@ export function EditorToolbar({
onRememberSelection,
onOpenLink,
onOpenMedia,
onInsertBlock,
}: Props) {
return (
<div
@@ -38,58 +73,65 @@ export function EditorToolbar({
}}
>
<div className="jio-creator__toolbar-group" role="group" aria-label={__('撤销和重做', 'jio')}>
<button type="button" className="jio-creator__tool-btn" disabled={!canUndo} onClick={() => onCommand('undo')} title={__('撤销', 'jio')} aria-label={__('撤销', 'jio')}>
<i className="fa fas fa-undo" aria-hidden="true" />
</button>
<button type="button" className="jio-creator__tool-btn" disabled={!canRedo} onClick={() => onCommand('redo')} title={__('重做', 'jio')} aria-label={__('重做', 'jio')}>
<i className="fa fas fa-redo" aria-hidden="true" />
</button>
<ToolButton label={__('撤销', 'jio')} icon="fa fas fa-undo" ready={canUndo} onClick={() => onCommand('undo')} />
<ToolButton label={__('重做', 'jio')} icon="fa fas fa-redo" ready={canRedo} onClick={() => onCommand('redo')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<select
className="jio-creator__format-select"
aria-label={__('段落样式', 'jio')}
value={formatValue}
disabled={!ready}
onFocus={onRememberSelection}
onChange={(event) => {
onFormatChange(event.target.value);
onCommand('format', event.target.value);
}}
>
<option value="p">{__('正文', 'jio')}</option>
<option value="h1">{__('标题 1', 'jio')}</option>
<option value="h2">{__('标题 2', 'jio')}</option>
<option value="h3">{__('标题 3', 'jio')}</option>
<option value="blockquote">{__('引用', 'jio')}</option>
</select>
<div className="jio-creator__toolbar-group" role="group" aria-label={__('段落样式', 'jio')}>
<label className="jio-creator__format-field">
<select
className="jio-creator__format-select"
aria-label={__('标题级别', 'jio')}
value={formatValue === 'blockquote' ? 'p' : formatValue}
disabled={!ready}
onFocus={onRememberSelection}
onChange={(event) => {
onFormatChange(event.target.value);
onCommand('format', event.target.value);
}}
>
<option value="p">{__('正文', 'jio')}</option>
<option value="h1">{__('标题 1', 'jio')}</option>
<option value="h2">{__('标题 2', 'jio')}</option>
<option value="h3">{__('标题 3', 'jio')}</option>
</select>
<i className="fa fas fa-heading" aria-hidden="true" />
<span className="jio-creator__tool-label">{__('标题', 'jio')}</span>
</label>
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('文字格式', 'jio')}>
<button type="button" className="jio-creator__tool-btn" disabled={!ready} onClick={() => onCommand('Bold')} title={__('加粗', 'jio')} aria-label={__('加粗', 'jio')}>
<i className="fa fas fa-bold" aria-hidden="true" />
</button>
<button type="button" className="jio-creator__tool-btn" disabled={!ready} onClick={() => onCommand('Italic')} title={__('斜体', 'jio')} aria-label={__('斜体', 'jio')}>
<i className="fa fas fa-italic" aria-hidden="true" />
</button>
<button type="button" className="jio-creator__tool-btn" disabled={!ready} onClick={() => onCommand('Underline')} title={__('下划线', 'jio')} aria-label={__('下划线', 'jio')}>
<i className="fa fas fa-underline" aria-hidden="true" />
</button>
<ToolButton label={__('加粗', 'jio')} icon="fa fas fa-bold" ready={ready} onClick={() => onCommand('Bold')} />
<ToolButton label={__('斜体', 'jio')} icon="fa fas fa-italic" ready={ready} onClick={() => onCommand('Italic')} />
<ToolButton label={__('下划线', 'jio')} icon="fa fas fa-underline" ready={ready} onClick={() => onCommand('Underline')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('段落块', 'jio')}>
<ToolButton label={__('列表', 'jio')} icon="fa fas fa-list-ul" ready={ready} onClick={() => onInsertBlock('list')} />
<ToolButton label={__('目录', 'jio')} icon="fa fas fa-list-ol" ready={ready} onClick={() => onInsertBlock('toc')} />
<ToolButton label={__('引用', 'jio')} icon="fa fas fa-quote-left" ready={ready} onClick={() => onCommand('format', 'blockquote')} />
<ToolButton label={__('分割线', 'jio')} icon="fa fas fa-minus" ready={ready} onClick={() => onInsertBlock('divider')} />
<ToolButton label={__('代码块', 'jio')} icon="fa fas fa-code" ready={ready} onClick={() => onInsertBlock('code')} />
<ToolButton label={__('注释', 'jio')} icon="fa fas fa-comment-dots" ready={ready} onClick={() => onInsertBlock('footnote')} />
<ToolButton label={__('公式', 'jio')} icon="fa fas fa-square-root-alt" ready={ready} onClick={() => onInsertBlock('math')} />
<ToolButton label={__('表格', 'jio')} icon="fa fas fa-table" ready={ready} onClick={() => onInsertBlock('table')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('媒体和链接', 'jio')}>
{capabilities.canUploadImage && (
<button type="button" className="jio-creator__tool-btn jio-creator__tool-labeled" disabled={!ready} onClick={() => onOpenMedia('image')} title={__('插入图片', 'jio')}>
<i className="fa far fa-image" aria-hidden="true" /><span>{__('图片', 'jio')}</span>
</button>
<ToolButton label={__('图片', 'jio')} icon="fa far fa-image" ready={ready} onClick={() => onOpenMedia('image')} />
)}
{(capabilities.canUploadVideo || capabilities.canEmbed) && (
<button type="button" className="jio-creator__tool-btn jio-creator__tool-labeled" disabled={!ready} onClick={() => onOpenMedia('video')} title={__('插入视频', 'jio')}>
<i className="fa fas fa-video" aria-hidden="true" /><span>{__('视频', 'jio')}</span>
</button>
<ToolButton label={__('视频', 'jio')} icon="fa fas fa-video" ready={ready} onClick={() => onOpenMedia('video')} />
)}
<ToolButton label={__('链接', 'jio')} icon="fa fas fa-link" ready={ready} onClick={onOpenLink} />
{capabilities.canUploadFile && (
<ToolButton label={__('附件', 'jio')} icon="fa fas fa-paperclip" ready={ready} onClick={() => onOpenMedia('file')} />
)}
<button type="button" className="jio-creator__tool-btn jio-creator__tool-labeled" disabled={!ready} onClick={onOpenLink} title={__('插入链接', 'jio')}>
<i className="fa fas fa-link" aria-hidden="true" /><span>{__('链接', 'jio')}</span>
</button>
</div>
</div>
);
+34 -14
View File
@@ -7,18 +7,18 @@
* @package Jio
*/
import { createRoot, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { createRoot, useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { apiPost } from './services/api';
import { PublishDrawer } from './panels/publish-drawer';
import { EditorToolbar } from './components/editor-toolbar';
import { PublishSettings } from './components/publish-settings';
import { AuthorRow } from './components/author-row';
import { SaveState, type SaveStateValue } from './components/save-state';
import { useAutosave } from './hooks/use-autosave';
import { useLocalRecovery } from './hooks/use-local-recovery';
import { normalizePayForRequest } from './utils/pay';
import { codeBlockMarkup, dividerMarkup, footnoteMarkup, listMarkup, mathMarkup, tableMarkup, tocMarkup } from './utils/block-markup';
import './styles/studio.scss';
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
@@ -184,8 +184,6 @@ function Studio(): JSX.Element {
setFormatValue(['h1', 'h2', 'h3', 'blockquote'].includes(name) ? name : 'p');
}, []);
const charCount = useMemo(() => Array.from(plainText(draft.content)).length, [draft.content]);
const buildPayload = useCallback((extra: Record<string, any> = {}) => {
const current = draftRef.current;
return {
@@ -510,7 +508,31 @@ function Studio(): JSX.Element {
}
}, [focusEditor]);
const openMedia = useCallback((type: 'image' | 'video') => {
const insertBlock = useCallback((kind: string) => {
const editor = focusEditor();
if (!editor) {
return;
}
const markup: Record<string, string> = {
list: listMarkup(false),
toc: tocMarkup(),
divider: dividerMarkup(),
code: codeBlockMarkup(),
footnote: footnoteMarkup(),
math: mathMarkup(),
table: tableMarkup(),
};
const html = markup[kind];
if (!html) {
return;
}
editor.insertContent(html);
editor.nodeChanged();
updateContent(editor.getContent());
updateHistoryAvailability(editor);
}, [focusEditor]);
const openMedia = useCallback((type: 'image' | 'video' | 'file') => {
const editor = editorRef.current;
const media = (window as any).JioSubmitMedia;
if (!editor || !media || typeof media.open !== 'function') {
@@ -535,6 +557,10 @@ function Studio(): JSX.Element {
const fullAttribute = fullUrl ? ` data-full-url="${escapeAttribute(fullUrl)}"` : '';
return `<p><img src="${escapeAttribute(url)}"${idAttribute} alt="${escapeAttribute(String(item.name || item.title || ''))}"${fullAttribute}></p>`;
}
if (type === 'file') {
const name = String(item.filename || item.name || url.split('/').pop() || '');
return `<p><a class="jio-file-link" href="${escapeAttribute(url)}"${idAttribute}><i class="fa fas fa-paperclip" aria-hidden="true"></i> ${escapeAttribute(name)}</a></p>`;
}
return `<div contenteditable="false" data-video-url="${escapeAttribute(url)}" data-video-name="${escapeAttribute(String(item.filename || item.name || url))}"${idAttribute} class="new-dplayer post-dplayer dplayer"></div><p></p>`;
}).join('');
if (markup) {
@@ -572,8 +598,8 @@ function Studio(): JSX.Element {
<EditorToolbar
ready={editorReady}
formatValue={formatValue}
onFormatChange={setFormatValue}
formatValue={formatValue}
onFormatChange={setFormatValue}
canUndo={historyAvailability.canUndo}
canRedo={historyAvailability.canRedo}
capabilities={capabilities}
@@ -581,6 +607,7 @@ function Studio(): JSX.Element {
onRememberSelection={rememberSelection}
onOpenLink={openLink}
onOpenMedia={openMedia}
onInsertBlock={insertBlock}
/>
<div className="jio-creator__bar-actions jio-creator__toolbar-extension" data-toolbar-extension aria-label={__('工具扩展区', 'jio')}>
<SaveState value={saveState} />
@@ -618,8 +645,6 @@ function Studio(): JSX.Element {
)}
<main className="jio-creator__canvas">
<AuthorRow author={bootstrap.author || {}} />
<label className="jio-creator__title">
<span className="screen-reader-text">{__('文章标题', 'jio')}</span>
<textarea
@@ -651,11 +676,6 @@ function Studio(): JSX.Element {
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
/>
<footer className="jio-creator__footer">
<span>{__('字数', 'jio')}:{charCount}</span>
<span>{__('约', 'jio')} {Math.max(1, Math.round(charCount / 400))} {__('分钟阅读', 'jio')}</span>
</footer>
{drawerOpen && (
<PublishDrawer
draft={draft}
+81 -98
View File
@@ -48,7 +48,7 @@ body.jio-page-submit.has-static-nav {
--jc-accent: #{$accent};
--jc-accent-strong: #{$accent-strong};
--jc-warn: #{$warn};
--jc-bar: 60px;
--jc-bar: 64px;
min-height: 100vh;
min-height: 100dvh;
@@ -172,25 +172,34 @@ body.jio-page-submit.has-static-nav {
background: var(--jc-line);
}
/* 工具按钮:图标在上、文字在下,一行高度固定,与公众号/知乎一致。 */
.jio-creator__tool-btn {
display: inline-flex;
min-width: 32px;
height: 34px;
width: auto;
min-width: 40px;
height: 46px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
gap: 7px;
padding: 0 8px;
flex-direction: column;
gap: 2px;
padding: 5px 6px;
border: 0;
border-radius: 7px;
border-radius: 6px;
background: transparent;
color: var(--jc-ink-soft);
color: #5f6664;
cursor: pointer;
font-size: 13px;
font-size: 11px;
line-height: 1.2;
i {
font-size: 14px;
line-height: 1;
}
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
background: #edf3f0;
background: #f0f2f1;
color: var(--jc-accent-strong);
outline: 0;
}
@@ -201,31 +210,61 @@ body.jio-page-submit.has-static-nav {
}
}
.jio-creator__tool-labeled span {
.jio-creator__tool-label {
max-width: 52px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jio-creator__format-select {
width: 94px;
height: 34px;
/* 标题按钮沿用「图标 + 文字」外观,内部叠一个原生的级别下拉。 */
.jio-creator__format-field {
position: relative;
display: inline-flex;
width: auto;
min-width: 48px;
height: 46px;
flex: 0 0 auto;
padding: 0 8px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--jc-ink-soft);
font: inherit;
font-size: 13px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 2px;
padding: 5px 6px;
border-radius: 6px;
cursor: pointer;
&:hover,
&:focus-visible {
border-color: var(--jc-line);
background: #fff;
outline: 0;
i {
font-size: 14px;
line-height: 1;
}
&:hover,
&:focus-within {
background: #f0f2f1;
color: var(--jc-accent-strong);
}
&:focus-within {
outline: 2px solid rgba(38, 118, 101, 0.35);
outline-offset: -2px;
}
}
.jio-creator__format-select {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
opacity: 0;
cursor: pointer;
&:disabled {
color: #aebbb5;
cursor: default;
}
}
@@ -475,7 +514,7 @@ body.jio-page-submit.has-static-nav {
.jio-creator__settings {
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
margin: 20px auto 0;
margin: 20px auto clamp(48px, 10vh, 140px);
padding: clamp(20px, 3vw, 28px) clamp(24px, 4vw, 40px);
border-radius: 4px;
background: #fff;
@@ -638,70 +677,14 @@ body.jio-page-submit.has-static-nav {
}
}
.jio-creator__footer {
display: flex;
justify-content: space-between;
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
margin: 16px auto 0;
padding: 0 4px 140px;
color: #98a19e;
font-size: 12px;
}
/* ---------- 作者行 ---------- */
.jio-creator__author {
display: flex;
align-items: center;
gap: 8px;
margin: 2px 0 20px;
color: #8c9a94;
font-size: 12px;
}
.jio-creator__author-avatar {
width: 24px;
height: 24px;
flex: 0 0 auto;
border-radius: 50%;
object-fit: cover;
}
.jio-creator__author-avatar.is-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
background: #e3ece7;
color: #4a5f57;
font-size: 11px;
font-weight: 600;
}
.jio-creator__author-name {
color: #4a5f57;
font-weight: 500;
}
.jio-creator__author-meta {
padding: 1px 6px;
border-radius: 4px;
background: #eef5f2;
color: #3c7d6c;
font-size: 11px;
}
.jio-creator__author-hint {
margin-left: auto;
color: #b0bcb7;
}
@import './_publish-drawer';
/* ---------- 手机 / 平板 ---------- */
@media (max-width: 767px) {
.jio-creator {
--jc-bar: 56px;
/* 图标 + 文字的两行按钮把顶栏撑高,移动端同样保留文字。 */
--jc-bar: 60px;
}
.jio-creator__bar {
@@ -736,17 +719,22 @@ body.jio-page-submit.has-static-nav {
}
.jio-creator__tool-btn {
min-width: 30px;
padding: 0 6px;
min-width: 36px;
padding: 4px 4px;
i {
font-size: 13px;
}
}
.jio-creator__tool-labeled span {
display: none;
.jio-creator__tool-label {
max-width: 42px;
font-size: 10px;
}
.jio-creator__format-select {
width: 82px;
padding: 0 5px;
.jio-creator__format-field {
min-width: 42px;
padding: 4px 4px;
}
.jio-creator__bar-actions .jio-creator__save {
@@ -788,7 +776,7 @@ body.jio-page-submit.has-static-nav {
.jio-creator__settings {
width: calc(100% - 20px);
margin-top: 12px;
margin: 12px auto calc(40px + env(safe-area-inset-bottom));
padding: 18px;
}
@@ -800,11 +788,6 @@ body.jio-page-submit.has-static-nav {
font-size: 24px;
}
.jio-creator__footer {
width: calc(100% - 20px);
padding-bottom: calc(120px + env(safe-area-inset-bottom));
}
.jio-creator__editor .mce-edit-area iframe,
.jio-creator__editor .tox .tox-edit-area__iframe {
min-height: 48vh;
@@ -0,0 +1,66 @@
/**
* 正文块的 HTML 片段。
*
* 这些块都由服务端再次清洗(FrontSubmitSave 的 wp_kses_post),因此这里只负责
* 生成符合前台渲染习惯的结构:代码块沿用 enlighter 的既有高亮约定,脚注与公式
* 使用主题前台已有的渲染口径,目录使用文章目录树识别标题的 data-nav 结构。
*
* @package Jio
*/
import { __ } from '@wordpress/i18n';
const escapeHtml = (value: string): string =>
value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
/** 有序 / 无序列表:直接插入一个空列表块,光标落在第一项。 */
export function listMarkup(ordered: boolean): string {
const tag = ordered ? 'ol' : 'ul';
return `<${tag}><li></li></${tag}><p></p>`;
}
/** 分割线。 */
export function dividerMarkup(): string {
return '<hr><p></p>';
}
/**
* 代码块:沿用前台 enlighter 的高亮标记,未选择语言时按通用代码展示。
*/
export function codeBlockMarkup(): string {
return (
'<pre class="enlighter-pre"><code class="gl" data-enlighter-language="generic">' +
escapeHtml(__('在此输入代码', 'jio')) +
'</code></pre><p></p>'
);
}
/**
* 表格:3×3 带表头,方便作者直接在单元格里改写。
*/
export function tableMarkup(): string {
const cell = (tag: string, text: string): string => `<${tag}>${escapeHtml(text)}</${tag}>`;
const header = `<tr>${[1, 2, 3].map((i) => cell('th', `${__('列', 'jio')} ${i}`)).join('')}</tr>`;
const rows = [1, 2]
.map(() => `<tr>${[1, 2, 3].map(() => cell('td', '')).join('')}</tr>`)
.join('');
return `<table class="jio-content-table"><thead>${header}</thead><tbody>${rows}</tbody></table><p></p>`;
}
/**
* 目录:文章页的目录树由前台 section-navs 依据 [data-nav] 生成,
* 因此这里只放一个占位标记,读者端不会看到裸露的标记文本。
*/
export function tocMarkup(): string {
return `<div class="jio-toc-placeholder"></div><p></p>`;
}
/** 注释:Markdown 脚注语法,前台由文章渲染流程转成编号脚注。 */
export function footnoteMarkup(): string {
return '<p>[^1]<br>[^1]: ' + escapeHtml(__('注释内容', 'jio')) + '</p>';
}
/** 公式:行内 LaTeX,包在主题可识别的公式容器里。 */
export function mathMarkup(): string {
return `<span class="jio-math">E = mc^2</span>`;
}
+74
View File
@@ -678,6 +678,80 @@ article.article.box-body,
overflow: hidden;
}
/* ---------- 正文扩展块:公式、脚注、附件、目录 ---------- */
.jio-math {
display: inline-block;
padding: 0 2px;
font-family: "Cambria Math", "Latin Modern Math", "Times New Roman", serif;
font-size: 1.02em;
white-space: pre-wrap;
}
.jio-math--block {
display: block;
margin: 12px 0 18px;
text-align: center;
}
.jio-footnotes {
margin-top: 32px;
padding-top: 18px;
border-top: 1px solid var(--main-border-color, rgba(50, 50, 50, 0.06));
font-size: 13px;
}
.jio-footnotes__title {
margin: 0 0 10px;
color: var(--muted-color, #777);
font-size: 13px;
font-weight: 600;
}
.jio-footnotes__list {
margin: 0;
padding-left: 20px;
color: var(--muted-color, #777);
line-height: 1.8;
}
.jio-footnotes__list li {
margin-bottom: 6px;
}
.jio-footnote-ref a,
.jio-footnote-back {
color: var(--theme-color, #267665);
text-decoration: none;
}
.jio-footnote-ref a:hover,
.jio-footnote-back:hover {
text-decoration: underline;
}
/* 编辑器插入的轻量附件链接 */
.wp-posts-content .jio-file-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--main-border-color, rgba(50, 50, 50, 0.08));
border-radius: var(--mini-radius, 6px);
background: var(--muted-bg-2-color, #f6f7f8);
text-decoration: none;
}
.wp-posts-content .jio-file-link i {
color: var(--theme-color, #267665);
}
/* 目录占位渲染成文章目录卡片时,卡片自带间距,这里只补上下留白。 */
.wp-posts-content .posts-nav-box {
margin: 20px 0;
}
/* source: assets/css/frontend/single-post/author-actions.css */
/* ===== Content Footer ===== */
.article-footer {
+1 -1
View File
@@ -1 +1 @@
.relative{position:relative}.flex{display:flex}.flex.ac,.ac{align-items:center}.flex.jc,.jc{justify-content:center}.flex.jsb,.jsb{justify-content:space-between}.flex.at,.at{align-items:flex-start}.flex.ae,.ae{align-items:flex-end}.flex0{flex:0 0 auto}.flex1{flex:1 1 auto;min-width:0}.flex.column{flex-direction:column}.flex.wrap{flex-wrap:wrap}.flex.flex-row,.flex-row{flex-direction:row}.flex.jse,.jse{justify-content:space-evenly}.flex.jsa,.jsa{justify-content:space-around}.flex>.flex1,.flex .flex1{flex:1;overflow:hidden;min-width:0}.relative-h{position:relative;height:100%}.padding-10{padding:10px}.font-bold{font-weight:700}.mt6{margin-top:6px}.gutters-5{margin:-5px}.gutters-5>*{padding:5px}@media(max-width:767px){.flex.flex-row.flex-col-sm-2{flex-wrap:wrap}.flex.flex-row.flex-col-sm-2>*{flex-basis:50%}}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}.abs-right{position:absolute;right:0;top:0}.abs-center{position:absolute;left:0;width:100%;top:50%;right:.7em;transform:translateY(-50%);z-index:1}.abs-center.left-top,.abs-center.right-top{transform:unset;top:0}.abs-center.right-top{left:auto;width:auto;text-align:right}.abs-center.left-top{text-align:left}.abs-center.conter-bottom,.abs-center.conter-conter{text-align:center}.abs-center.right-top,.abs-center.right-bottom,.abs-center.right-conter{text-align:right}.abs-center.conter-bottom,.abs-center.right-bottom,.abs-center.left-bottom{top:auto;transform:unset;bottom:0}.abs-center.left-bottom{left:0;right:auto;text-align:left}.abs-center.right-bottom,.abs-center.right-conter{left:auto;right:0;text-align:right}.graphic,.page-cover{text-shadow:0 0 5px rgba(0,0,0,.2);--main-color: #fff;color:#fff;position:relative;overflow:hidden;box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));padding:0 0 70%;border-radius:var(--main-radius, var(--radius, 8px))}.graphic>.fit-cover,.graphic>img.fit-cover{position:absolute;width:100%;height:100%;object-fit:cover;inset:0}.fit-cover{object-fit:cover;max-width:100%}.title-h-left{position:relative;padding-bottom:8px}.title-h-left:before{transition:.4s;transform-origin:left;position:absolute;content:"";width:40px;height:3px;background:var(--theme-color, var(--focus-color, #f04494));left:0;bottom:3px;border-radius:5px;box-shadow:1px 1px 3px -1px var(--theme-color, var(--focus-color, #f04494))}.line-form-line,.line-form-line:before{position:absolute;bottom:0;left:0;height:1px;width:100%;transition:.8s;background:var(--main-border-color, rgba(50, 50, 50, .06))}.line-form-line:before{content:"";width:0;background:var(--focus-color, #f04494)}.avatar-img{position:relative;display:inline-block;flex-shrink:0;--this-size: 38px;width:var(--this-size);height:var(--this-size)}.avatar-img .avatar,.avatar-img img{width:100%;height:100%;border-radius:50%;object-fit:cover}.avatar-lg{--this-size: 80px}.avatar-md{--this-size: 55px}.avatar-mini{--this-size: 20px}.shrink0{flex-shrink:0}.opacity8{opacity:.8}.muted-color{color:var(--muted-color, var(--color-ink-3))}.muted-2-color{color:var(--muted-2-color, var(--color-ink-4))}.muted-3-color{color:var(--muted-3-color, #b1b1b1)}.muted-4-color{color:var(--muted-4-color, #d2d2d2)}.text-center{text-align:center}.em09{font-size:.9em}.em12{font-size:1.2em}.ml3{margin-left:3px}.ml6{margin-left:6px}.ml10{margin-left:10px}.mr3{margin-right:3px}.mr6{margin-right:6px}.mt10{margin-top:10px}.mt20{margin-top:20px}.mb10{margin-bottom:10px}.mb20{margin-bottom:20px}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.text-ellipsis-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.main-bg{background-color:var(--main-bg-color, var(--surface-1))}.separator{position:relative;letter-spacing:.08em}.icon-circle+.icon-circle:before{content:"·";display:inline-block;margin:0 .45em;opacity:.55}.article-avatar name,.article-avatar .name,.user-name{display:inline-flex;align-items:center;gap:4px;min-width:0;max-width:100%}.pull-right{float:right}.pull-left{float:left}body.single main.container .content-layout{display:block;min-width:0}body.single main.container .content-layout>*{min-width:0;max-width:100%}body.single main.container .content-layout>.breadcrumb,body.single main.container .content-layout>ul.breadcrumb,body.single main.container .content-layout>.breadcrumb-nav,body.single main.container .content-layout>.breadcrumbs-wrap{position:relative;z-index:1;margin-top:0;margin-bottom:12px;padding-top:2px}body.single .sidebar .jio-panel,body.single .sidebar .widget{margin-bottom:15px}body.single main.container{box-sizing:border-box}.article,article.article.box-body,.article.main-bg.box-body{padding:0 30px;overflow:hidden}@media(max-width:767px){.article,article.article.box-body,.article.main-bg.box-body{padding:0 15px}.article-header .article-title{font-size:18px}}.entry-header,.article-header{margin-bottom:10px;padding-bottom:0;border-bottom:none;background:transparent;box-shadow:none;boLine truncated
.relative{position:relative}.flex{display:flex}.flex.ac,.ac{align-items:center}.flex.jc,.jc{justify-content:center}.flex.jsb,.jsb{justify-content:space-between}.flex.at,.at{align-items:flex-start}.flex.ae,.ae{align-items:flex-end}.flex0{flex:0 0 auto}.flex1{flex:1 1 auto;min-width:0}.flex.column{flex-direction:column}.flex.wrap{flex-wrap:wrap}.flex.flex-row,.flex-row{flex-direction:row}.flex.jse,.jse{justify-content:space-evenly}.flex.jsa,.jsa{justify-content:space-around}.flex>.flex1,.flex .flex1{flex:1;overflow:hidden;min-width:0}.relative-h{position:relative;height:100%}.padding-10{padding:10px}.font-bold{font-weight:700}.mt6{margin-top:6px}.gutters-5{margin:-5px}.gutters-5>*{padding:5px}@media(max-width:767px){.flex.flex-row.flex-col-sm-2{flex-wrap:wrap}.flex.flex-row.flex-col-sm-2>*{flex-basis:50%}}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}.abs-right{position:absolute;right:0;top:0}.abs-center{position:absolute;left:0;width:100%;top:50%;right:.7em;transform:translateY(-50%);z-index:1}.abs-center.left-top,.abs-center.right-top{transform:unset;top:0}.abs-center.right-top{left:auto;width:auto;text-align:right}.abs-center.left-top{text-align:left}.abs-center.conter-bottom,.abs-center.conter-conter{text-align:center}.abs-center.right-top,.abs-center.right-bottom,.abs-center.right-conter{text-align:right}.abs-center.conter-bottom,.abs-center.right-bottom,.abs-center.left-bottom{top:auto;transform:unset;bottom:0}.abs-center.left-bottom{left:0;right:auto;text-align:left}.abs-center.right-bottom,.abs-center.right-conter{left:auto;right:0;text-align:right}.graphic,.page-cover{text-shadow:0 0 5px rgba(0,0,0,.2);--main-color: #fff;color:#fff;position:relative;overflow:hidden;box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));padding:0 0 70%;border-radius:var(--main-radius, var(--radius, 8px))}.graphic>.fit-cover,.graphic>img.fit-cover{position:absolute;width:100%;height:100%;object-fit:cover;inset:0}.fit-cover{object-fit:cover;max-width:100%}.title-h-left{position:relative;padding-bottom:8px}.title-h-left:before{transition:.4s;transform-origin:left;position:absolute;content:"";width:40px;height:3px;background:var(--theme-color, var(--focus-color, #f04494));left:0;bottom:3px;border-radius:5px;box-shadow:1px 1px 3px -1px var(--theme-color, var(--focus-color, #f04494))}.line-form-line,.line-form-line:before{position:absolute;bottom:0;left:0;height:1px;width:100%;transition:.8s;background:var(--main-border-color, rgba(50, 50, 50, .06))}.line-form-line:before{content:"";width:0;background:var(--focus-color, #f04494)}.avatar-img{position:relative;display:inline-block;flex-shrink:0;--this-size: 38px;width:var(--this-size);height:var(--this-size)}.avatar-img .avatar,.avatar-img img{width:100%;height:100%;border-radius:50%;object-fit:cover}.avatar-lg{--this-size: 80px}.avatar-md{--this-size: 55px}.avatar-mini{--this-size: 20px}.shrink0{flex-shrink:0}.opacity8{opacity:.8}.muted-color{color:var(--muted-color, var(--color-ink-3))}.muted-2-color{color:var(--muted-2-color, var(--color-ink-4))}.muted-3-color{color:var(--muted-3-color, #b1b1b1)}.muted-4-color{color:var(--muted-4-color, #d2d2d2)}.text-center{text-align:center}.em09{font-size:.9em}.em12{font-size:1.2em}.ml3{margin-left:3px}.ml6{margin-left:6px}.ml10{margin-left:10px}.mr3{margin-right:3px}.mr6{margin-right:6px}.mt10{margin-top:10px}.mt20{margin-top:20px}.mb10{margin-bottom:10px}.mb20{margin-bottom:20px}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.text-ellipsis-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.main-bg{background-color:var(--main-bg-color, var(--surface-1))}.separator{position:relative;letter-spacing:.08em}.icon-circle+.icon-circle:before{content:"·";display:inline-block;margin:0 .45em;opacity:.55}.article-avatar name,.article-avatar .name,.user-name{display:inline-flex;align-items:center;gap:4px;min-width:0;max-width:100%}.pull-right{float:right}.pull-left{float:left}body.single main.container .content-layout{display:block;min-width:0}body.single main.container .content-layout>*{min-width:0;max-width:100%}body.single main.container .content-layout>.breadcrumb,body.single main.container .content-layout>ul.breadcrumb,body.single main.container .content-layout>.breadcrumb-nav,body.single main.container .content-layout>.breadcrumbs-wrap{position:relative;z-index:1;margin-top:0;margin-bottom:12px;padding-top:2px}body.single .sidebar .jio-panel,body.single .sidebar .widget{margin-bottom:15px}body.single main.container{box-sizing:border-box}.article,article.article.box-body,.article.main-bg.box-body{padding:0 30px;overflow:hidden}@media(max-width:767px){.article,article.article.box-body,.article.main-bg.box-body{padding:0 15px}.article-header .article-title{font-size:18px}}.entry-header,.article-header{margin-bottom:10px;padding-bottom:0;border-bottom:none;background:transparent;box-shadow:none;boLine truncated
@@ -340,3 +340,76 @@ article.article.box-body,
overflow: hidden;
}
/* ---------- 正文扩展块:公式、脚注、附件、目录 ---------- */
.jio-math {
display: inline-block;
padding: 0 2px;
font-family: "Cambria Math", "Latin Modern Math", "Times New Roman", serif;
font-size: 1.02em;
white-space: pre-wrap;
}
.jio-math--block {
display: block;
margin: 12px 0 18px;
text-align: center;
}
.jio-footnotes {
margin-top: 32px;
padding-top: 18px;
border-top: 1px solid var(--main-border-color, rgba(50, 50, 50, 0.06));
font-size: 13px;
}
.jio-footnotes__title {
margin: 0 0 10px;
color: var(--muted-color, #777);
font-size: 13px;
font-weight: 600;
}
.jio-footnotes__list {
margin: 0;
padding-left: 20px;
color: var(--muted-color, #777);
line-height: 1.8;
}
.jio-footnotes__list li {
margin-bottom: 6px;
}
.jio-footnote-ref a,
.jio-footnote-back {
color: var(--theme-color, #267665);
text-decoration: none;
}
.jio-footnote-ref a:hover,
.jio-footnote-back:hover {
text-decoration: underline;
}
/* 编辑器插入的轻量附件链接 */
.wp-posts-content .jio-file-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--main-border-color, rgba(50, 50, 50, 0.08));
border-radius: var(--mini-radius, 6px);
background: var(--muted-bg-2-color, #f6f7f8);
text-decoration: none;
}
.wp-posts-content .jio-file-link i {
color: var(--theme-color, #267665);
}
/* 目录占位渲染成文章目录卡片时,卡片自带间距,这里只补上下留白。 */
.wp-posts-content .posts-nav-box {
margin: 20px 0;
}
+3
View File
@@ -324,6 +324,9 @@ if (class_exists('\Jio\Core\CreatorRest')) {
if (class_exists('\Jio\Core\CreatorStudio')) {
\Jio\Core\CreatorStudio::get_instance()->init();
}
if (class_exists('\Jio\Content\ContentBlocks')) {
\Jio\Content\ContentBlocks::get_instance()->init();
}
if (class_exists('\Jio\Content\PrivateAssets')) {
\Jio\Content\PrivateAssets::get_instance()->init();
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* 正文扩展块的渲染:目录、脚注、公式。
*
* 编辑器插入的都是 HTML/文本占位,读者端由这里转换成真正的渲染结果:
* - 目录:占位替换成前台 section-navs 识别的 .posts-nav-box 卡片;
* - 脚注:[^1] 引用与 [^1]: 定义转换成脚注列表并重新编号;
* - 公式:.jio-math 内的 ^ / _ 转成上标下标,给出可读的数学排版。
*
* @package Jio
*/
namespace Jio\Content;
defined('ABSPATH') || exit;
class ContentBlocks {
/** 编辑器插入的目录占位 class。 */
public const TOC_CLASS = 'jio-toc-placeholder';
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_filter('the_content', [$this, 'render_blocks'], 9);
}
/**
* 正文里是否插入了目录占位(用于决定是否加载目录脚本)。
*/
public static function has_toc_placeholder(string $content): bool {
return strpos($content, self::TOC_CLASS) !== false;
}
public function render_blocks(string $content): string {
if ($content === '' || is_admin()) {
return $content;
}
$content = $this->render_toc($content);
$content = $this->render_footnotes($content);
return $this->render_math($content);
}
/**
* 目录占位 → 文章目录卡片。
*/
private function render_toc(string $content): string {
if (!self::has_toc_placeholder($content)) {
return $content;
}
$box = '<div class="widget-container"><div class="posts-nav-box" data-title="'
. esc_attr__('本文目录', 'jio')
. '"></div></div>';
$pattern = '/<div[^>]*class="[^"]*' . preg_quote(self::TOC_CLASS, '/') . '[^"]*"[^>]*>\s*<\/div>\s*(?:<p>\s*<\/p>)?/i';
$replaced = preg_replace($pattern, $box, $content, 1);
return is_string($replaced) ? $replaced : $content;
}
/**
* Markdown 脚注:[^1] 引用与 [^1]: 定义。
*/
private function render_footnotes(string $content): string {
if (strpos($content, '[^') === false) {
return $content;
}
if (!preg_match_all('/\[\^([^\]]+)\]:[ \t]*([^\n<]+)/u', $content, $defs, PREG_SET_ORDER)) {
return $content;
}
$notes = [];
foreach ($defs as $def) {
$notes[(string) $def[1]] = trim((string) $def[2]);
$content = str_replace($def[0], '', $content);
}
if ($notes === []) {
return $content;
}
// 按首次引用顺序编号,而不是按定义顺序。
$order = [];
$result = preg_replace_callback('/\[\^([^\]]+)\]/u', static function (array $match) use (&$order): string {
$key = (string) $match[1];
if (!isset($order[$key])) {
$order[$key] = count($order) + 1;
}
$index = $order[$key];
return '<sup class="jio-footnote-ref"><a href="#jio-fn-' . $index . '" id="jio-fnref-' . $index . '">'
. $index
. '</a></sup>';
}, $content);
if (!is_string($result)) {
return $content;
}
$content = $result;
$items = '';
foreach ($order as $key => $index) {
if (!isset($notes[$key])) {
continue;
}
$items .= '<li id="jio-fn-' . $index . '">'
. esc_html($notes[$key])
. ' <a class="jio-footnote-back" href="#jio-fnref-' . $index . '" aria-label="'
. esc_attr__('返回正文', 'jio')
. '">↩</a></li>';
}
if ($items === '') {
return $content;
}
return $content
. '<section class="jio-footnotes"><h4 class="jio-footnotes__title">'
. esc_html__('注释', 'jio')
. '</h4><ol class="jio-footnotes__list">' . $items . '</ol></section>';
}
/**
* 公式:把 .jio-math 片段里的 ^ 与 _ 转成真正可见的上标下标。
*
* 主题没有内置 TeX 引擎,这里只做确定性的公式排版,不猜测其它语法。
*/
private function render_math(string $content): string {
if (strpos($content, 'jio-math') === false) {
return $content;
}
$result = preg_replace_callback(
'#<span([^>]*\bclass="[^"]*jio-math[^"]*"[^>]*)>(.*?)</span>#is',
static function (array $match): string {
$inner = esc_html(html_entity_decode($match[2], ENT_QUOTES, 'UTF-8'));
$inner = (string) preg_replace('/\^\{([^{}]*)\}|\^(\S)/u', '<sup>$1$2</sup>', $inner);
$inner = (string) preg_replace('/_\{([^{}]*)\}|_(\S)/u', '<sub>$1$2</sub>', $inner);
return '<span' . $match[1] . '>' . $inner . '</span>';
},
$content
);
return is_string($result) ? $result : $content;
}
}
-33
View File
@@ -153,8 +153,6 @@ final class CreatorRest {
'titlePlaceholder' => (string) $settings['title_placeholder'],
'contentPlaceholder' => (string) $settings['content_placeholder'],
],
// 作者行数据由服务端给出,前端不自行拼装昵称/头像。
'author' => $this->author_payload($editable, $user_id),
'privateUploads' => $this->private_upload_rows($user_id, $post_id),
'privateStorage' => PrivateAssets::is_available(),
'media' => [
@@ -566,37 +564,6 @@ final class CreatorRest {
];
}
/**
* 作者行数据:昵称、头像与身份说明。
*
* @return array{name:string,avatar:string,meta:string}
*/
private function author_payload(?\WP_Post $editable, int $user_id): array {
$author_id = $editable instanceof \WP_Post ? (int) $editable->post_author : $user_id;
$user = $author_id > 0 ? get_userdata($author_id) : null;
if (!$user) {
return ['name' => '', 'avatar' => '', 'meta' => ''];
}
$avatar = function_exists('jio_get_avatar_url') ? (string) jio_get_avatar_url($author_id, ['size' => 64]) : '';
if ($avatar === '' && function_exists('get_avatar_url')) {
$avatar = (string) get_avatar_url($author_id, ['size' => 64]);
}
$meta = '';
if ($author_id !== $user_id) {
$meta = __('原作者', 'jio');
} elseif (function_exists('jio_get_user_vip_level') && (int) jio_get_user_vip_level($user_id) > 0) {
$meta = sprintf(__('VIP%s', 'jio'), (string) max(1, min(2, (int) jio_get_user_vip_level($user_id))));
}
return [
'name' => (string) $user->display_name,
'avatar' => $avatar,
'meta' => $meta,
];
}
/**
* @param array<string, mixed> $post_meta
* @return array<string, mixed>
+4 -1
View File
@@ -103,7 +103,10 @@ trait ScriptsPart1c {
}
$requirements['is_article'] = $is_article;
$requirements['has_toc'] = $is_article && $article_nav_enabled;
// 用户在正文里插入了目录块时,即使关闭了侧栏目录树也要加载目录脚本。
$has_inline_toc = class_exists('\Jio\Content\ContentBlocks')
&& \Jio\Content\ContentBlocks::has_toc_placeholder($content);
$requirements['has_toc'] = $is_article && ($article_nav_enabled || $has_inline_toc);
$requirements['has_video'] = $has_video;
$requirements['has_hidden_content'] = $has_hidden_content || $has_yiyan || $has_inserted_content;
$requirements['needs_pay_box'] = $is_paid_content_single || (bool) preg_match('/\[(?:payshow|hidecontent)\b|wp:jio\/paid-fragment/i', $content);
@@ -150,7 +150,7 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
assert.match(appSource, /undoManager\.undo\(\)/);
assert.match(appSource, /undoManager\.redo\(\)/);
assert.match(appSource, /editor\.formatter\.toggle\(value \|\| 'h2'\)/);
assert.match(editorToolbar, /value=\{formatValue\}/);
assert.match(editorToolbar, /value=\{formatValue === 'blockquote' \? 'p' : formatValue\}/);
assert.match(editorToolbar, /canUndo/);
assert.match(editorToolbar, /canRedo/);
assert.match(editorToolbar, /onCommand\('Bold'\)/);
@@ -158,6 +158,19 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
assert.match(editorToolbar, /onOpenMedia\('image'\)/);
assert.match(editorToolbar, /onOpenMedia\('video'\)/);
assert.match(editorToolbar, /onClick=\{onOpenLink\}/);
// 引用不再是标题下拉的一部分,而是独立按钮。
assert.match(editorToolbar, /onCommand\('format', 'blockquote'\)/);
assert.doesNotMatch(editorToolbar, /<option value="blockquote"/);
// 新增的块级按钮。
for (const kind of ['list', 'toc', 'divider', 'code', 'footnote', 'math', 'table']) {
assert.ok(editorToolbar.includes(`onInsertBlock('${kind}')`), `missing block button: ${kind}`);
}
assert.match(appSource, /onInsertBlock=\{insertBlock\}/);
assert.match(read('assets/creator-studio/src/utils/block-markup.ts'), /jio-toc-placeholder/);
assert.match(read('assets/creator-studio/src/utils/block-markup.ts'), /jio-math/);
// 工具栏按钮统一为「图标在上、文字在下」。
assert.match(editorToolbar, /jio-creator__tool-label/);
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__tool-btn\s*\{[\s\S]*?flex-direction:\s*column/);
assert.match(appSource, /保存草稿/);
assert.match(appSource, /<SaveState value=\{saveState\}/);
assert.match(appSource, /draftRef\.current\.postId/);
@@ -237,7 +250,15 @@ test('scheduled publishing is validated and applied server side', () => {
assert.match(rest, /'canSchedule'\s*=>\s*\$cap\('new_post_audit_no'\)/);
});
test('Chinese character count strips markup and counts visible text', () => {
assert.match(appSource, /Array\.from\(plainText\(draft\.content\)\)\.length/);
assert.match(appSource, /container\.textContent/);
test('the writing card has no author strip above the title', () => {
assert.doesNotMatch(appSource, /AuthorRow/);
assert.doesNotMatch(appSource, /原创内容/);
assert.doesNotMatch(read('assets/creator-studio/src/styles/studio.scss'), /jio-creator__author/);
});
test('the writing surface has no character or reading-time footer', () => {
assert.doesNotMatch(appSource, /jio-creator__footer/);
assert.doesNotMatch(appSource, /分钟阅读/);
assert.doesNotMatch(appSource, /charCount/);
assert.doesNotMatch(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__footer/);
});
@@ -708,7 +708,7 @@ test('single articles enqueue only content and settings driven assets', () => {
assert.match(singleBlock, new RegExp(`singular_assets\\['${requirement}'\\]`));
}
assert.match(runtime, /\$requirements\['has_toc'\] = \$is_article && \$article_nav_enabled/);
assert.match(runtime, /\$requirements\['has_toc'\] = \$is_article && \(\$article_nav_enabled \|\| \$has_inline_toc\)/);
assert.match(runtime, /data-jio-video/);
assert.match(runtime, /post_password_required\(\$post_id\)/);
assert.match(runtime, /comment_settings\['close_comments'\]/);
@@ -43,7 +43,10 @@ test('TOC script enqueue follows article visibility settings and per-post overri
assert.match(scripts, /\$article_nav_enabled = !array_key_exists\('article_nav', \$settings\) \|\| !empty\(\$settings\['article_nav'\]\)/);
assert.match(scripts, /get_post_meta\(\$post_id, DomainModel::POST_META_KEY, true\)/);
assert.match(scripts, /empty\(\$extension_meta\['no_article-navs'\]\)/);
assert.match(scripts, /\$requirements\['has_toc'\] = \$is_article && \$article_nav_enabled/);
// 正文里插入目录块时也要加载脚本,因此条件是「设置开启」或「正文含占位」。
assert.match(scripts, /\$has_inline_toc = class_exists\(/);
assert.match(scripts, /ContentBlocks::has_toc_placeholder\(\$content\)/);
assert.match(scripts, /\$requirements\['has_toc'\] = \$is_article && \(\$article_nav_enabled \|\| \$has_inline_toc\)/);
assert.doesNotMatch(scripts, /preg_match_all\('\/<h\[1-4\]/);
assert.match(scripts, /\$singular_assets\['has_toc'\][\s\S]*?'jio-section-navs'/);