feat(creator-studio): full pay settings and paid resources in the publish drawer

The drawer only had a pay toggle and one bare price field, so several
options from the front-submit pay card could not be set at all.

- New PayPanel mirrors the front-submit card: cash / points segment
  (points only when the site has credits enabled), price or points with
  the currency suffix, and VIP prices. VIP fields render only when the
  site asks for manual VIP pricing and the level is enabled, and now
  submit vip1_points / vip2_points as well.
- New ResourceDialog restores "添加资源": download URL with an attachment
  picker, resource note (extraction code), copy name / value, custom
  button label and button style swatches. Pasting a net-disk share link
  splits the URL and the extraction code.
- pay.ts sends every download display field and tags net-disk links.
- CreatorRest exposes vipManual / vip*Enabled / vip names / pointsEnabled
  / downloadClasses / currencySymbol, and the readback carries
  extractCode / copyKey / copyVal / class / attachmentId.
- Fixes an existing data loss: snapshot_from_payload dropped copy_key,
  copy_val, class and attachment_id, so approving a revision erased them.

Files split to stay within budget: publish-drawer 332 lines, pay-panel
343, resource-dialog 254. 886 widget-system tests pass.
This commit is contained in:
kate522 committed 2026-11-09 15:24:30 +08:00
1 parent c5bdbc0457
commit 0e4e4670dd
12 files changed
+1022 -224

No files matched your search

+1 -1
View File
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'c9b50dfd71946ad961a9');
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '736777902cbeff44d3e4');
+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-top:112px;--jc-status:52px;-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-bottom:var(--jc-status);padding-top:var(--jc-top)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__top{background:rgba(250,251,250,.98);box-shadow:0 2px 8px rgba(27,46,39,.035);left:0;position:fixed;right:0;top:0;z-index:300}.jio-creator__masthead{align-items:center;display:flex;gap:16px;height:56px;justify-content:space-between;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__masthead-left{align-items:center;display:flex;gap:10px;min-width:0}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;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__brand{align-items:center;color:inherit;display:inline-flex;min-width:0;text-decoration:none}.jio-creator__brand:focus-visible{outline:2px solid rgba(38,118,101,.35);outline-offset:4px}.jio-creator__brand-logo{display:block;height:30px;max-width:168px;-o-object-fit:contain;object-fit:contain;width:auto}.jio-creator__brand-text{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:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__me{align-items:center;border-radius:50%;display:inline-flex;flex:0 0 auto;justify-content:center}.jio-creator__me:focus-visible{outline:2px solid rgba(38,118,101,.5);outline-offset:2px}.jio-creator__me-avatar{background:#eef1f0;border:1px solid rgba(24,39,34,.08);border-radius:50%;display:block;height:32px;-o-object-fit:cover;object-fit:cover;overflow:hidden;width:32px}.jio-creator__me-avatar.is-placeholder{align-items:center;color:#9aa2a0;display:inline-flex;font-size:13px;justify-content:center}.jio-creator__bar{align-items:center;border-bottom:1px solid var(--jc-line);border-top:1px solid var(--jc-line);display:flex;height:56px;justify-content:center;justify-content:safe center;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__toolbar-tools{align-items:center;display:flex;gap:7px;max-width:100%;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.is-active:not(:disabled),.jio-creator__tool-btn.is-open:not(:disabled){background:#e9f1ee;color:var(--jc-accent-strong)}.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__tool-caret{bottom:4px;color:#b0b9b6;font-size:8px;line-height:1;position:absolute;right:3px}.jio-creator__tool-menu{display:inline-flex;flex:0 0 auto;position:relative}.jio-creator__tool-menu .jio-creator__tool-btn{position:relative}.jio-creator__menu{animation:jio-menu-in .12s ease-out;background:#fff;border:1px solid rgba(24,39,34,.06);border-radius:12px;box-shadow:0 14px 40px rgba(20,32,28,.16),0 2px 6px rgba(20,32,28,.06);display:flex;flex-direction:column;gap:2px;padding:6px;position:fixed;z-index:620}.jio-creator__menu.is-top{animation-name:jio-menu-in-up}@keyframes jio-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes jio-menu-in-up{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.jio-creator__menu-item{align-items:center;background:transparent;border:0;border-radius:7px;color:#4Line 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-top:112px;--jc-status:52px;-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-bottom:var(--jc-status);padding-top:var(--jc-top)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__top{background:rgba(250,251,250,.98);box-shadow:0 2px 8px rgba(27,46,39,.035);left:0;position:fixed;right:0;top:0;z-index:300}.jio-creator__masthead{align-items:center;display:flex;gap:16px;height:56px;justify-content:space-between;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__masthead-left{align-items:center;display:flex;gap:10px;min-width:0}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;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__brand{align-items:center;color:inherit;display:inline-flex;min-width:0;text-decoration:none}.jio-creator__brand:focus-visible{outline:2px solid rgba(38,118,101,.35);outline-offset:4px}.jio-creator__brand-logo{display:block;height:30px;max-width:168px;-o-object-fit:contain;object-fit:contain;width:auto}.jio-creator__brand-text{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:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__me{align-items:center;border-radius:50%;display:inline-flex;flex:0 0 auto;justify-content:center}.jio-creator__me:focus-visible{outline:2px solid rgba(38,118,101,.5);outline-offset:2px}.jio-creator__me-avatar{background:#eef1f0;border:1px solid rgba(24,39,34,.08);border-radius:50%;display:block;height:32px;-o-object-fit:cover;object-fit:cover;overflow:hidden;width:32px}.jio-creator__me-avatar.is-placeholder{align-items:center;color:#9aa2a0;display:inline-flex;font-size:13px;justify-content:center}.jio-creator__bar{align-items:center;border-bottom:1px solid var(--jc-line);border-top:1px solid var(--jc-line);display:flex;height:56px;justify-content:center;justify-content:safe center;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__toolbar-tools{align-items:center;display:flex;gap:7px;max-width:100%;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.is-active:not(:disabled),.jio-creator__tool-btn.is-open:not(:disabled){background:#e9f1ee;color:var(--jc-accent-strong)}.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__tool-caret{bottom:4px;color:#b0b9b6;font-size:8px;line-height:1;position:absolute;right:3px}.jio-creator__tool-menu{display:inline-flex;flex:0 0 auto;position:relative}.jio-creator__tool-menu .jio-creator__tool-btn{position:relative}.jio-creator__menu{animation:jio-menu-in .12s ease-out;background:#fff;border:1px solid rgba(24,39,34,.06);border-radius:12px;box-shadow:0 14px 40px rgba(20,32,28,.16),0 2px 6px rgba(20,32,28,.06);display:flex;flex-direction:column;gap:2px;padding:6px;position:fixed;z-index:620}.jio-creator__menu.is-top{animation-name:jio-menu-in-up}@keyframes jio-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes jio-menu-in-up{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.jio-creator__menu-item{align-items:center;background:transparent;border:0;border-radius:7px;color:#4Line truncated
+9 -8
View File
@@ -1,8 +1,9 @@
(()=>{"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 o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await o(r);if(!r.ok)throw n(i,r.status);return i}async function i(e,t,a,r={}){const i=new FormData;i.append("file",t),Object.entries(r).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 o(l);if(!l.ok)throw n(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:d,taxonomy:u,isPublishedEdit:m,onClose:p,onSaveDraft:f,onSubmit:_,privateStorage:b,privateUploads:h,saving:g,captcha:y}=n,[j,v]=(0,t.useState)(""),[E,k]=(0,t.useState)(""),[w,N]=(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)(()=>(o.title||"").trim().length,[o.title])>=Number(d.titleMin||1),M=()=>{const e=j.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),v("")};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===u.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),u.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),u.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"},o.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:()=>r({tags:o.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:j,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>v(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),M())}}),(0,e.createElement)("button",{type:"button",onClick:M},(0,a.__)("添加","jio")))),u.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=o.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:()=>r({custom:{...o.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(_.timezone||"UTC")))))}function j({centerUrl:t,site:n,viewer:o}){const r=String(n.name||""),i=String(o.avatar||""),l=t||"/";return(0,e.createElement)("header",{className:"jio-creator__masthead"},(0,e.createElement)("div",{className:"jio-creator__masthead-left"},(0,e.createElement)("a",{className:"jio-creator__back",href:t||"/","aria-label":(0,a.__)("返回作者中心","jio")},(0,e.createElement)("i",{className:"fa fas fa-angle-left","aria-hidden":"true"})),(0,e.createElement)("a",{className:"jio-creator__brand",href:String(n.home||"/"),"aria-label":r||(0,a.__)("返回首页","jio")},n.logo?(0,e.createElement)("img",{className:"jio-creator__brand-logo",src:String(n.logo),alt:r}):(0,e.createElement)("span",{className:"jio-creator__brand-text"},r))),(0,e.createElement)("a",{className:"jio-creator__me",href:l,"aria-label":(0,a.__)("返回作者中心","jio")},i?(0,e.createElement)("img",{className:"jio-creator__me-avatar",src:i,alt:"",width:32,height:32}):(0,e.createElement)("span",{className:"jio-creator__me-avatar is-placeholder","aria-hidden":"true"},(0,e.createElement)("i",{className:"fa fas fa-user"}))))}function v(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,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function k(e){if(!e||-1===e.indexOf("data-jio-"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("[data-jio-math-source]").forEach(e=>{const t=e.getAttribute("data-jio-math-source")||"",a="0"!==e.getAttribute("data-jio-math-display"),n=`[jio_math display="${a?"1":"0"}"]${t}[/jio_math]`,o=e.closest("p");a&&o&&o.textContent?.trim()===e.textContent?.trim()?o.textContent=n:e.replaceWith(document.createTextNode(n))}),t.querySelectorAll("[data-jio-footnote]").forEach(e=>{const t=e.getAttribute("data-jio-footnote")||"";e.replaceWith(document.createTextNode(`[jio_footnote]${t}[/jio_footnote]`))}),t.innerHTML}function w(e){return e&&-1!==e.indexOf("[jio_")?e.replace(/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,(e,t,a)=>"0"===t?`<span class="jio-math" data-jio-math-source="${E(a)}" data-jio-math-display="0" contenteditable="false">${N(a)}</span>`:`<p><span class="jio-math jio-math--block" data-jio-math-source="${E(a)}" data-jio-math-display="1" contenteditable="false">${N(a)}</span></p>`).replace(/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,(e,t)=>`<sup class="jio-footnote-ref" data-jio-footnote="${E((e=>{const t=document.createElement("textarea");return t.innerHTML=e,t.value})(t))}" contenteditable="false">${t}</sup>`):e}const N=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\^\{([^{}]*)\}|\^(\S)/gu,"<sup>$1$2</sup>").replace(/_\{([^{}]*)\}|_(\S)/gu,"<sub>$1$2</sub>"),C={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 S({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,C[t.state]||C.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}const x=36e5,M=24*x,$=30*M,L=365*M,q={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 R({charCount:t,saveState:n,isPublishedEdit:o,saving:r,onSaveDraft:i,onPreview:l,onPublish:c}){const s=n.at?function(e,t=Date.now()){const n=Date.parse(e);if(!Number.isFinite(n))return"";const o=Math.max(0,t-n);return o<6e4?(0,a.__)("刚刚","jio"):o<x?(0,a.sprintf)(/* translators: %d: minutes */ /* translators: %d: minutes */
(0,a.__)("%d分钟前","jio"),Math.floor(o/6e4)):o<M?(0,a.sprintf)(/* translators: %d: hours */ /* translators: %d: hours */
(0,a.__)("%d小时前","jio"),Math.floor(o/x)):o<$?(0,a.sprintf)(/* translators: %d: days */ /* translators: %d: days */
(0,a.__)("%d天前","jio"),Math.floor(o/M)):o<L?(0,a.sprintf)(/* translators: %d: months */ /* translators: %d: months */
(0,a.__)("%d个月前","jio"),Math.floor(o/$)):(0,a.sprintf)(/* translators: %d: years */ /* translators: %d: years */
(0,a.__)("%d年前","jio"),Math.floor(o/L))}(n.at):"",d=q[n.state]||q.ready;return(0,e.createElement)("footer",{className:"jio-creator__status",role:"contentinfo"},(0,e.createElement)("div",{className:"jio-creator__status-left"},(0,e.createElement)("span",{className:"jio-creator__status-count"},(0,a.__)("字数","jio"),":",t),(0,e.createElement)("span",{className:"jio-creator__status-mode"},(0,a.__)("Markdown 语法输入中","jio"))),(0,e.createElement)("div",{className:"jio-creator__status-meta"},(0,e.createElement)(S,{value:n}),(0,e.createElement)("span",{className:"jio-creator__status-dot","aria-hidden":"true"},"·"),(0,e.createElement)("span",{className:"jio-creator__status-time"},s||d)),(0,e.createElement)("div",{className:"jio-creator__status-actions"},(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:i,disabled:r},(0,a.__)("保存草稿","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:l,disabled:r},(0,a.__)("预览","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-primary",onClick:c,disabled:r},o?(0,a.__)("提交改稿","jio"):(0,a.__)("发布","jio"))))}const I=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),D=/<(?:p|div|h[1-6]|ul|ol|li|blockquote|pre|table|figure|section)\b/i,P=e=>{const t=e.trim();return""===t?"<p></p>":D.test(t)?t:`<p>${t}</p>`};const z={reply:{open:"[reply]",close:"[/reply]",label:(0,a.__)("评论后可查看","jio")},login:{open:'[jio_hide mode="login"]',close:"[/jio_hide]",label:(0,a.__)("登录后可查看","jio")},vip:{open:"[vip]",close:"[/vip]",label:(0,a.__)("会员可查看","jio")},payshow:{open:"[payshow]",close:"[/payshow]",label:(0,a.__)("付费后可查看","jio")}},U=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),A={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function B({kind:n,onSubmit:o,onClose:r}){const[i,l]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[d,u]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[f,_]=(0,t.useState)("a^2 + b^2 = c^2"),[b,h]=(0,t.useState)(!0),[g,y]=(0,t.useState)(""),j=(0,t.useRef)(null),v=(0,t.useRef)(null),E=(0,t.useRef)(null),k=(0,t.useRef)(null);(0,t.useEffect)(()=>{(v.current||E.current||k.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const w=(0,t.useMemo)(()=>"footnote"===n?""!==g.trim():"math"!==n||""!==f.trim(),[n,g,f]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:j},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},A[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:v,type:"number",min:1,max:20,value:i,onChange:e=>l(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:d,onChange:e=>u(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:E,rows:3,value:f,onChange:e=>_(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("支持上标 ^ 与下标 _,例如 x^2 与 a_n;暂不支持完整 LaTeX。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:b,onChange:e=>h(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("独占一行显示","Line truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(ne.titleMax)||100),spellCheck:!1,maxLength:Number(ne.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":p?"true":"false"},(0,e.createElement)("textarea",{id:J,defaultValue:n.content,onChange:e=>le(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(y,{featured:n.featured,canEditCover:!!ae.canUploadImage,canSchedule:!!ae.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>ie({featured:e}),onScheduleChange:(e,t)=>ie({schedule:{enabled:e,at:t}})}),D&&(0,e.createElement)(B,{kind:D,onSubmit:xe,onClose:()=>U(null)}),(0,e.createElement)(R,{charCount:re,saveState:M,isPublishedEdit:oe,saving:!p||"saving"===M.state||"submitting"===M.state,onSaveDraft:()=>{me(!1)},onPreview:()=>{fe()},onPublish:()=>q(!0)}),L&&(0,e.createElement)(s,{draft:n,setDraft:ie,capabilities:ae,limits:ne,taxonomy:{categories:H.categories&&H.categories.items||[],tags:!!H.tags?.enabled,customDefs:H.customDefs||[]},isPublishedEdit:oe,onClose:()=>q(!1),onSaveDraft:()=>{me(!1)},onSubmit:e=>{pe(e)},privateStorage:!!H.privateStorage,privateUploads:H.privateUploads||[],captcha:!!H.captcha?.enabled,saving:"saving"===M.state||"submitting"===M.state}))}function G(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(Q,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",G):G()})();
(()=>{"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 o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await o(r);if(!r.ok)throw n(i,r.status);return i}async function i(e,t,a,r={}){const i=new FormData;i.append("file",t),Object.entries(r).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 o(l);if(!l.ok)throw n(c,l.status);return c}const l={url:"",attachmentId:0,extractCode:"",copyKey:"",copyVal:"",label:"",class:""};function c({row:n,buttonClasses:o,onPickAttachment:r,onSubmit:i,onClose:c}){const[s,d]=(0,t.useState)(()=>n?{url:String(n.url||""),attachmentId:Number(n.attachmentId||0),extractCode:String(n.extractCode||""),copyKey:String(n.copyKey||""),copyVal:String(n.copyVal||""),label:String(n.label||""),class:String(n.class||"")}:{...l}),[u,m]=(0,t.useState)(""),p=(0,t.useRef)(null);(0,t.useEffect)(()=>{p.current?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),c())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[c]);const _=e=>d(t=>({...t,...e}));return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-resource-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:c}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel jio-creator-dialog__panel--wide"},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-resource-title"},n?(0,a.__)("编辑资源","jio"):(0,a.__)("添加资源","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:c,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("下载地址","jio")),(0,e.createElement)("div",{className:"jio-creator-dialog__url"},(0,e.createElement)("input",{ref:p,type:"url",value:s.url,placeholder:(0,a.__)("请输入资源下载地址","jio"),onChange:e=>_({url:e.target.value,attachmentId:0}),onPaste:e=>{const t=e.clipboardData?.getData("text")||"";if(!t)return;const n=t.match(/https?:\/\/\S+/i),o=t.match(/(?:提取码|密码)\s*[::]\s*([A-Za-z0-9]{4,8})/);if(!n&&!o)return;e.preventDefault();const r={attachmentId:0};n&&(r.url=n[0].replace(/[)\],。]+$/u,"")),o&&(r.extractCode=(0,a.sprintf)(/* translators: %s: extraction code */ /* translators: %s: extraction code */
(0,a.__)("提取码:%s","jio"),o[1]),r.copyKey=(0,a.__)("提取码","jio"),r.copyVal=o[1]),_(r)}}),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__ghost",onClick:()=>r((e,t)=>_({url:e,attachmentId:t}))},(0,e.createElement)("i",{className:"fa fas fa-upload","aria-hidden":"true"})," ",(0,a.__)("上传附件","jio"))),(0,e.createElement)("span",{className:"jio-creator-dialog__hint"},(0,a.__)("支持直接粘贴网盘分享链接,会自动识别提取码。","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("资源备注","jio")),(0,e.createElement)("input",{type:"text",value:s.extractCode,placeholder:(0,a.__)("例如提取码、解压密码等","jio"),onChange:e=>_({extractCode:e.target.value})})),(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("复制名称","jio")),(0,e.createElement)("input",{type:"text",value:s.copyKey,placeholder:(0,a.__)("如:提取码","jio"),onChange:e=>_({copyKey:e.target.value})})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("复制内容","jio")),(0,e.createElement)("input",{type:"text",value:s.copyVal,placeholder:(0,a.__)("如:abcd","jio"),onChange:e=>_({copyVal:e.target.value})}))),(0,e.createElement)("span",{className:"jio-creator-dialog__hint"},(0,a.__)("填写后读者可点击按钮复制这段内容。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("自定义按钮文案","jio")),(0,e.createElement)("input",{type:"text",value:s.label,placeholder:(0,a.__)("留空则按网盘自动识别","jio"),onChange:e=>_({label:e.target.value})})),(0,e.createElement)("div",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("按钮样式","jio")),(0,e.createElement)("div",{className:"jio-creator-swatches"},o.map(t=>(0,e.createElement)("button",{key:t||"default",type:"button",className:`jio-creator-swatch ${t||"b-theme"}${s.class===t?" is-active":""}`,onClick:()=>_({class:t}),title:t||(0,a.__)("默认样式","jio"),"aria-label":t||(0,a.__)("默认样式","jio"),"aria-pressed":s.class===t})))),u&&(0,e.createElement)("p",{className:"jio-creator-error"},u)),(0,e.createElement)("footer",{className:"jio-creator-dialog__foot"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__ghost",onClick:c},(0,a.__)("取消","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__primary",onClick:()=>{const e=function(e){const t=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""}}(s.url);if(!e)return void m((0,a.__)("请填写有效的下载地址","jio"));m("");const t=/pan\.baidu\.com|aliyundrive\.com|alipan\.com|pan\.quark\.cn|cloud\.189\.cn|115\.com|lanzou|weiyun|123pan/i.test(e);i({...n||{},type:t?"net_disk":"direct",url:e,attachmentId:Math.max(0,Number(s.attachmentId)||0),extractCode:s.extractCode.trim(),copyKey:s.copyKey.trim(),copyVal:s.copyVal.trim(),label:s.label.trim(),class:s.class})}},(0,a.__)("确认","jio")))))}const s=(e,t=0)=>{const a=Number(e);return Number.isFinite(a)?a:t};function d({pay:n,onChange:o,canDownload:r,privateStorage:i,privateUploads:l,bootstrap:d,onUploadPrivate:u,uploading:m,uploadError:p,onPickAttachment:_}){const[f,b]=(0,t.useState)(null),h=(0,t.useRef)(null),g=e=>o({...n,...e}),j="points"===n.modo,y=String(d.currencySymbol||"¥"),v=d.downloadClasses&&d.downloadClasses.length>0?d.downloadClasses:["","c-blue","b-blue","jb-blue"],E=n.downloads,k=e=>j?(0,a.sprintf)(/* translators: %s: VIP level name */ /* translators: %s: VIP level name */(0,a.__)("%s 积分","jio"),e):(0,a.sprintf)(/* translators: %s: VIP level name */ /* translators: %s: VIP level name */(0,a.__)("%s 价格","jio"),e),w=(t,a,n,o)=>{var r;return(0,e.createElement)("label",{className:"jio-creator-input"},(0,e.createElement)("span",null,t),(0,e.createElement)("span",{className:"jio-creator-price"},o.prefix&&(0,e.createElement)("em",null,o.prefix),(0,e.createElement)("input",{type:"number",min:null!==(r=o.min)&&void 0!==r?r:0,step:o.step,value:a,placeholder:o.placeholder,onChange:e=>n(e.target.value)}),o.suffix&&(0,e.createElement)("em",null,o.suffix)))};return(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("开启后,未购买读者只能看到免费部分;添加下载资源后同时出售下载权限。","jio"))),(0,e.createElement)("label",{className:"jio-creator-toggle"},(0,e.createElement)("input",{type:"checkbox",checked:n.enabled,onChange:e=>g({enabled:e.target.checked})}),(0,e.createElement)("span",{"aria-hidden":"true"}))),n.enabled&&(0Line truncated
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(j.timezone||"UTC")))))}function w({centerUrl:t,site:n,viewer:o}){const r=String(n.name||""),i=String(o.avatar||""),l=t||"/";return(0,e.createElement)("header",{className:"jio-creator__masthead"},(0,e.createElement)("div",{className:"jio-creator__masthead-left"},(0,e.createElement)("a",{className:"jio-creator__back",href:t||"/","aria-label":(0,a.__)("返回作者中心","jio")},(0,e.createElement)("i",{className:"fa fas fa-angle-left","aria-hidden":"true"})),(0,e.createElement)("a",{className:"jio-creator__brand",href:String(n.home||"/"),"aria-label":r||(0,a.__)("返回首页","jio")},n.logo?(0,e.createElement)("img",{className:"jio-creator__brand-logo",src:String(n.logo),alt:r}):(0,e.createElement)("span",{className:"jio-creator__brand-text"},r))),(0,e.createElement)("a",{className:"jio-creator__me",href:l,"aria-label":(0,a.__)("返回作者中心","jio")},i?(0,e.createElement)("img",{className:"jio-creator__me-avatar",src:i,alt:"",width:32,height:32}):(0,e.createElement)("span",{className:"jio-creator__me-avatar is-placeholder","aria-hidden":"true"},(0,e.createElement)("i",{className:"fa fas fa-user"}))))}function N(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.label?e.label:"",a="string"==typeof e.assetKey?e.assetKey:"";if(a)return{type:"protected",asset_key:a,label:t};const n="string"==typeof e.url?e.url.trim():"";return n?{type:"net_disk"===e.type?"net_disk":"direct",url:n,label:t,extract_code:"string"==typeof e.extractCode?e.extractCode:"",copy_key:"string"==typeof e.copyKey?e.copyKey:"",copy_val:"string"==typeof e.copyVal?e.copyVal:"",class:"string"==typeof e.class?e.class:"",attachment_id:Math.max(0,Number(e.attachmentId)||0)}:null}).filter(Boolean),a="points"===e.modo;return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:a?"points":"0",price:a?"0.00":String(e.price||"0.00"),points_price:a?Math.max(0,Number(e.points)||0):0,vip1_price:a?"0.00":String(e.vip1Price||"0.00"),vip2_price:a?"0.00":String(e.vip2Price||"0.00"),vip1_points:a?Math.max(0,Number(e.vip1Points)||0):0,vip2_points:a?Math.max(0,Number(e.vip2Points)||0):0,pay_limit:Number(e.payLimit)||0,downloads:t}}}const C=e=>e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function S(e){if(!e||-1===e.indexOf("data-jio-"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("[data-jio-math-source]").forEach(e=>{const t=e.getAttribute("data-jio-math-source")||"",a="0"!==e.getAttribute("data-jio-math-display"),n=`[jio_math display="${a?"1":"0"}"]${t}[/jio_math]`,o=e.closest("p");a&&o&&o.textContent?.trim()===e.textContent?.trim()?o.textContent=n:e.replaceWith(document.createTextNode(n))}),t.querySelectorAll("[data-jio-footnote]").forEach(e=>{const t=e.getAttribute("data-jio-footnote")||"";e.replaceWith(document.createTextNode(`[jio_footnote]${t}[/jio_footnote]`))}),t.innerHTML}function x(e){return e&&-1!==e.indexOf("[jio_")?e.replace(/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,(e,t,a)=>"0"===t?`<span class="jio-math" data-jio-math-source="${C(a)}" data-jio-math-display="0" contenteditable="false">${M(a)}</span>`:`<p><span class="jio-math jio-math--block" data-jio-math-source="${C(a)}" data-jio-math-display="1" contenteditable="false">${M(a)}</span></p>`).replace(/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,(e,t)=>`<sup class="jio-footnote-ref" data-jio-footnote="${C((e=>{const t=document.createElement("textarea");return t.innerHTML=e,t.value})(t))}" contenteditable="false">${t}</sup>`):e}const M=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\^\{([^{}]*)\}|\^(\S)/gu,"<sup>$1$2</sup>").replace(/_\{([^{}]*)\}|_(\S)/gu,"<sub>$1$2</sub>"),$={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 L({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,$[t.state]||$.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}const q=36e5,P=24*q,I=30*P,R=365*P,D={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 z({charCount:t,saveState:n,isPublishedEdit:o,saving:r,onSaveDraft:i,onPreview:l,onPublish:c}){const s=n.at?function(e,t=Date.now()){const n=Date.parse(e);if(!Number.isFinite(n))return"";const o=Math.max(0,t-n);return o<6e4?(0,a.__)("刚刚","jio"):o<qLine truncated
(0,a.__)("%d分钟前","jio"),Math.floor(o/6e4)):o<P?(0,a.sprintf)(/* translators: %d: hours */ /* translators: %d: hours */
(0,a.__)("%d小时前","jio"),Math.floor(o/q)):o<I?(0,a.sprintf)(/* translators: %d: days */ /* translators: %d: days */
(0,a.__)("%d天前","jio"),Math.floor(o/P)):o<R?(0,a.sprintf)(/* translators: %d: months */ /* translators: %d: months */
(0,a.__)("%d个月前","jio"),Math.floor(o/I)):(0,a.sprintf)(/* translators: %d: years */ /* translators: %d: years */
(0,a.__)("%d年前","jio"),Math.floor(o/R))}(n.at):"",d=D[n.state]||D.ready;return(0,e.createElement)("footer",{className:"jio-creator__status",role:"contentinfo"},(0,e.createElement)("div",{className:"jio-creator__status-left"},(0,e.createElement)("span",{className:"jio-creator__status-count"},(0,a.__)("字数","jio"),":",t),(0,e.createElement)("span",{className:"jio-creator__status-mode"},(0,a.__)("Markdown 语法输入中","jio"))),(0,e.createElement)("div",{className:"jio-creator__status-meta"},(0,e.createElement)(L,{value:n}),(0,e.createElement)("span",{className:"jio-creator__status-dot","aria-hidden":"true"},"·"),(0,e.createElement)("span",{className:"jio-creator__status-time"},s||d)),(0,e.createElement)("div",{className:"jio-creator__status-actions"},(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:i,disabled:r},(0,a.__)("保存草稿","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:l,disabled:r},(0,a.__)("预览","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-primary",onClick:c,disabled:r},o?(0,a.__)("提交改稿","jio"):(0,a.__)("发布","jio"))))}const A=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),U=/<(?:p|div|h[1-6]|ul|ol|li|blockquote|pre|table|figure|section)\b/i,V=e=>{const t=e.trim();return""===t?"<p></p>":U.test(t)?t:`<p>${t}</p>`};const B={reply:{open:"[reply]",close:"[/reply]",label:(0,a.__)("评论后可查看","jio")},login:{open:'[jio_hide mode="login"]',close:"[/jio_hide]",label:(0,a.__)("登录后可查看","jio")},vip:{open:"[vip]",close:"[/vip]",label:(0,a.__)("会员可查看","jio")},payshow:{open:"[payshow]",close:"[/payshow]",label:(0,a.__)("付费后可查看","jio")}},K=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),T={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function F({kind:n,onSubmit:o,onClose:r}){const[i,l]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[d,u]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,h]=(0,t.useState)(!0),[g,j]=(0,t.useState)(""),y=(0,t.useRef)(null),v=(0,t.useRef)(null),E=(0,t.useRef)(null),k=(0,t.useRef)(null);(0,t.useEffect)(()=>{(v.current||E.current||k.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const w=(0,t.useMemo)(()=>"footnote"===n?""!==g.trim():"math"!==n||""!==_.trim(),[n,g,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},T[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:v,type:"number",min:1,max:20,value:i,onChange:e=>l(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:d,onChange:e=>u(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:E,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("支持上标 ^ 与下标 _,例如 x^2 与 a_n;暂不支持完整 LaTeX。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:b,onChange:e=>h(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("独占一行显示","Line truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(ne.titleMax)||100),spellCheck:!1,maxLength:Number(ne.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":m?"true":"false"},(0,e.createElement)("textarea",{id:Q,defaultValue:n.content,onChange:e=>le(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(k,{featured:n.featured,canEditCover:!!ae.canUploadImage,canSchedule:!!ae.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>ie({featured:e}),onScheduleChange:(e,t)=>ie({schedule:{enabled:e,at:t}})}),P&&(0,e.createElement)(F,{kind:P,onSubmit:xe,onClose:()=>I(null)}),(0,e.createElement)(z,{charCount:re,saveState:M,isPublishedEdit:oe,saving:!m||"saving"===M.state||"submitting"===M.state,onSaveDraft:()=>{me(!1)},onPreview:()=>{_e()},onPublish:()=>q(!0)}),L&&(0,e.createElement)(p,{draft:n,setDraft:ie,capabilities:ae,limits:ne,taxonomy:{categories:W.categories&&W.categories.items||[],tags:!!W.tags?.enabled,customDefs:W.customDefs||[]},isPublishedEdit:oe,onClose:()=>q(!1),onSaveDraft:()=>{me(!1)},onSubmit:e=>{pe(e)},privateStorage:!!W.privateStorage,privateUploads:W.privateUploads||[],captcha:!!W.captcha?.enabled,saving:"saving"===M.state||"submitting"===M.state}))}function ae(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(te,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",ae):ae()})();
@@ -0,0 +1,254 @@
/**
* 付费资源编辑器。
*
* 与前台投稿页的「添加资源」弹窗保持同一组字段:下载地址、资源备注(提取码)、
* 复制名称 / 内容、自定义按钮文案、按钮样式,以及从媒体库挑一个普通附件。
* 粘贴网盘分享链接时自动拆分地址与提取码,和原实现一致。
*
* @package Jio
*/
import { useEffect, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import type { DownloadRow } from '../draft';
type Props = {
/** 编辑已有资源时传入;为空表示新增。 */
row: DownloadRow | null;
buttonClasses: string[];
/** 打开媒体库选普通附件(可上传),返回 URL 与附件 ID。 */
onPickAttachment: (apply: (url: string, attachmentId: number) => void) => void;
onSubmit: (row: DownloadRow) => void;
onClose: () => void;
};
type Fields = {
url: string;
attachmentId: number;
extractCode: string;
copyKey: string;
copyVal: string;
label: string;
class: string;
};
const EMPTY: Fields = {
url: '',
attachmentId: 0,
extractCode: '',
copyKey: '',
copyVal: '',
label: '',
class: '',
};
function safeHttpUrl(value: string): string {
const raw = value.trim();
if (!raw) {
return '';
}
try {
const url = new URL(raw, window.location.href);
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
} catch (error) {
return '';
}
}
export function ResourceDialog({ row, buttonClasses, onPickAttachment, onSubmit, onClose }: Props) {
const [fields, setFields] = useState<Fields>(() =>
row
? {
url: String(row.url || ''),
attachmentId: Number(row.attachmentId || 0),
extractCode: String(row.extractCode || ''),
copyKey: String(row.copyKey || ''),
copyVal: String(row.copyVal || ''),
label: String(row.label || ''),
class: String(row.class || ''),
}
: { ...EMPTY }
);
const [error, setError] = useState('');
const urlRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
urlRef.current?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
onClose();
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onClose]);
const patch = (next: Partial<Fields>) => setFields((current) => ({ ...current, ...next }));
/** 粘贴分享文本:拆出地址与「提取码:xxxx」。 */
const onPaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
const text = event.clipboardData?.getData('text') || '';
if (!text) {
return;
}
const urlMatch = text.match(/https?:\/\/\S+/i);
const codeMatch = text.match(/(?:提取码|密码)\s*[::]\s*([A-Za-z0-9]{4,8})/);
if (!urlMatch && !codeMatch) {
return;
}
event.preventDefault();
const next: Partial<Fields> = { attachmentId: 0 };
if (urlMatch) {
next.url = urlMatch[0].replace(/[)\],。]+$/u, '');
}
if (codeMatch) {
next.extractCode = sprintf(
/* translators: %s: extraction code */
__('提取码:%s', 'jio'),
codeMatch[1]
);
next.copyKey = __('提取码', 'jio');
next.copyVal = codeMatch[1];
}
patch(next);
};
const submit = () => {
const url = safeHttpUrl(fields.url);
if (!url) {
setError(__('请填写有效的下载地址', 'jio'));
return;
}
setError('');
const isNetDisk = /pan\.baidu\.com|aliyundrive\.com|alipan\.com|pan\.quark\.cn|cloud\.189\.cn|115\.com|lanzou|weiyun|123pan/i.test(
url
);
onSubmit({
...(row || {}),
type: isNetDisk ? 'net_disk' : 'direct',
url,
attachmentId: Math.max(0, Number(fields.attachmentId) || 0),
extractCode: fields.extractCode.trim(),
copyKey: fields.copyKey.trim(),
copyVal: fields.copyVal.trim(),
label: fields.label.trim(),
class: fields.class,
});
};
return (
<div className="jio-creator-dialog" role="dialog" aria-modal="true" aria-labelledby="jio-creator-resource-title">
<button type="button" className="jio-creator-dialog__backdrop" aria-label={__('关闭', 'jio')} onClick={onClose} />
<div className="jio-creator-dialog__panel jio-creator-dialog__panel--wide">
<header className="jio-creator-dialog__head">
<h2 id="jio-creator-resource-title">{row ? __('编辑资源', 'jio') : __('添加资源', 'jio')}</h2>
<button type="button" className="jio-creator-dialog__close" onClick={onClose} aria-label={__('关闭', 'jio')}>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</header>
<div className="jio-creator-dialog__body">
<label className="jio-creator-dialog__field">
<span>{__('下载地址', 'jio')}</span>
<div className="jio-creator-dialog__url">
<input
ref={urlRef}
type="url"
value={fields.url}
placeholder={__('请输入资源下载地址', 'jio')}
onChange={(event) => patch({ url: event.target.value, attachmentId: 0 })}
onPaste={onPaste}
/>
<button
type="button"
className="jio-creator-dialog__ghost"
onClick={() =>
onPickAttachment((url, attachmentId) => patch({ url, attachmentId }))
}
>
<i className="fa fas fa-upload" aria-hidden="true" /> {__('上传附件', 'jio')}
</button>
</div>
<span className="jio-creator-dialog__hint">
{__('支持直接粘贴网盘分享链接,会自动识别提取码。', 'jio')}
</span>
</label>
<label className="jio-creator-dialog__field">
<span>{__('资源备注', 'jio')}</span>
<input
type="text"
value={fields.extractCode}
placeholder={__('例如提取码、解压密码等', 'jio')}
onChange={(event) => patch({ extractCode: event.target.value })}
/>
</label>
<div className="jio-creator-dialog__row">
<label>
<span>{__('复制名称', 'jio')}</span>
<input
type="text"
value={fields.copyKey}
placeholder={__('如:提取码', 'jio')}
onChange={(event) => patch({ copyKey: event.target.value })}
/>
</label>
<label>
<span>{__('复制内容', 'jio')}</span>
<input
type="text"
value={fields.copyVal}
placeholder={__('如:abcd', 'jio')}
onChange={(event) => patch({ copyVal: event.target.value })}
/>
</label>
</div>
<span className="jio-creator-dialog__hint">
{__('填写后读者可点击按钮复制这段内容。', 'jio')}
</span>
<label className="jio-creator-dialog__field">
<span>{__('自定义按钮文案', 'jio')}</span>
<input
type="text"
value={fields.label}
placeholder={__('留空则按网盘自动识别', 'jio')}
onChange={(event) => patch({ label: event.target.value })}
/>
</label>
<div className="jio-creator-dialog__field">
<span>{__('按钮样式', 'jio')}</span>
<div className="jio-creator-swatches">
{buttonClasses.map((value) => (
<button
key={value || 'default'}
type="button"
className={`jio-creator-swatch ${value || 'b-theme'}${fields.class === value ? ' is-active' : ''}`}
onClick={() => patch({ class: value })}
title={value || __('默认样式', 'jio')}
aria-label={value || __('默认样式', 'jio')}
aria-pressed={fields.class === value}
/>
))}
</div>
</div>
{error && <p className="jio-creator-error">{error}</p>}
</div>
<footer className="jio-creator-dialog__foot">
<button type="button" className="jio-creator-dialog__ghost" onClick={onClose}>
{__('取消', 'jio')}
</button>
<button type="button" className="jio-creator-dialog__primary" onClick={submit}>
{__('确认', 'jio')}
</button>
</footer>
</div>
</div>
);
}
+21 -1
View File
@@ -8,6 +8,22 @@
export type Featured = { type: string; data: Record<string, any>; mediaId?: number };
/** 付费资源行:与前台投稿的资源编辑器字段一一对应。 */
export type DownloadRow = {
type: 'protected' | 'direct' | 'net_disk';
assetKey?: string;
url?: string;
label?: string;
fileName?: string;
sizeBytes?: number;
sizeLabel?: string;
extractCode?: string;
copyKey?: string;
copyVal?: string;
class?: string;
attachmentId?: number;
};
export type PayState = {
enabled: boolean;
modo: 'cash' | 'points';
@@ -15,8 +31,10 @@ export type PayState = {
points: number;
vip1Price: string;
vip2Price: string;
vip1Points: number;
vip2Points: number;
payLimit: number;
downloads: Array<Record<string, any>>;
downloads: DownloadRow[];
};
export type Limits = {
@@ -67,6 +85,8 @@ export function initialDraft(bootstrap: Record<string, any>): Draft {
points: Number(pay.points || 0),
vip1Price: String(pay.vip1Price || '0.00'),
vip2Price: String(pay.vip2Price || '0.00'),
vip1Points: Number(pay.vip1Points || 0),
vip2Points: Number(pay.vip2Points || 0),
payLimit: Number(pay.payLimit || 0),
downloads: Array.isArray(pay.downloads) ? pay.downloads : [],
},
@@ -0,0 +1,343 @@
/**
* 发布抽屉里的付费配置。
*
* 与前台投稿页的「付费内容」卡片对齐:现金 / 积分切换、普通价、会员价
* (手动填写或按折扣自动计算)、以及付费下载资源列表。服务端仍是价格的
* 唯一裁决者(最低 / 最高价、会员价不得高于普通价),这里只负责收集输入。
*
* @package Jio
*/
import { useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import type { DownloadRow, PayState } from '../draft';
import { ResourceDialog } from '../components/resource-dialog';
export type PayBootstrap = {
freeAllowed?: boolean;
vipManual?: boolean;
vip1Enabled?: boolean;
vip2Enabled?: boolean;
vip1Name?: string;
vip2Name?: string;
pointsEnabled?: boolean;
downloadClasses?: string[];
currencySymbol?: string;
priceMin?: number;
priceMax?: number;
pointsMin?: number;
pointsMax?: number;
};
type Props = {
pay: PayState;
onChange: (pay: PayState) => void;
canDownload: boolean;
privateStorage: boolean;
privateUploads: Array<Record<string, any>>;
bootstrap: PayBootstrap;
onUploadPrivate: (file: File) => Promise<DownloadRow | null>;
uploading: boolean;
uploadError: string;
onPickAttachment: (apply: (url: string, attachmentId: number) => void) => void;
};
const toNumber = (value: string, fallback = 0): number => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
export function PayPanel({
pay,
onChange,
canDownload,
privateStorage,
privateUploads,
bootstrap,
onUploadPrivate,
uploading,
uploadError,
onPickAttachment,
}: Props) {
const [editing, setEditing] = useState<{ row: DownloadRow | null; index: number } | null>(null);
const fileInput = useRef<HTMLInputElement | null>(null);
const patch = (next: Partial<PayState>) => onChange({ ...pay, ...next });
const points = pay.modo === 'points';
const symbol = String(bootstrap.currencySymbol || '¥');
const buttonClasses = bootstrap.downloadClasses && bootstrap.downloadClasses.length > 0
? bootstrap.downloadClasses
: ['', 'c-blue', 'b-blue', 'jb-blue'];
const uploads = pay.downloads;
const addUploaded = async (file: File | undefined) => {
if (!file) {
return;
}
const row = await onUploadPrivate(file);
if (row) {
patch({ enabled: true, downloads: [...uploads, row] });
}
if (fileInput.current) {
fileInput.current.value = '';
}
};
const applyResource = (row: DownloadRow) => {
const index = editing?.index ?? -1;
const next = index >= 0
? uploads.map((item, position) => (position === index ? row : item))
: [...uploads, row];
setEditing(null);
patch({ enabled: true, downloads: next });
};
const priceLabel = (name: string) =>
points
? sprintf(/* translators: %s: VIP level name */ __('%s 积分', 'jio'), name)
: sprintf(/* translators: %s: VIP level name */ __('%s 价格', 'jio'), name);
const renderPriceInput = (
label: string,
value: string,
onInput: (next: string) => void,
options: { min?: number; step: string; suffix?: string; prefix?: string; placeholder?: string }
) => (
<label className="jio-creator-input">
<span>{label}</span>
<span className="jio-creator-price">
{options.prefix && <em>{options.prefix}</em>}
<input
type="number"
min={options.min ?? 0}
step={options.step}
value={value}
placeholder={options.placeholder}
onChange={(event) => onInput(event.target.value)}
/>
{options.suffix && <em>{options.suffix}</em>}
</span>
</label>
);
return (
<>
<div className="jio-creator-switch">
<div>
<h3>{__('付费内容', 'jio')}</h3>
<p className="jio-creator-muted">
{__('开启后,未购买读者只能看到免费部分;添加下载资源后同时出售下载权限。', 'jio')}
</p>
</div>
<label className="jio-creator-toggle">
<input
type="checkbox"
checked={pay.enabled}
onChange={(event) => patch({ enabled: event.target.checked })}
/>
<span aria-hidden="true" />
</label>
</div>
{pay.enabled && (
<div className="jio-creator-pay">
{bootstrap.pointsEnabled && (
<div className="jio-creator-segment" role="group" aria-label={__('支付类型', 'jio')}>
<button
type="button"
className={pay.modo === 'cash' ? 'is-active' : ''}
aria-pressed={pay.modo === 'cash'}
onClick={() => patch({ modo: 'cash' })}
>
{__('现金支付', 'jio')}
</button>
<button
type="button"
className={pay.modo === 'points' ? 'is-active' : ''}
aria-pressed={pay.modo === 'points'}
onClick={() => patch({ modo: 'points' })}
>
{__('积分支付', 'jio')}
</button>
</div>
)}
{points
? renderPriceInput(__('设置积分', 'jio'), String(pay.points), (next) => patch({ points: toNumber(next) }), {
min: Number(bootstrap.pointsMin || 0),
step: '1',
suffix: __('积分', 'jio'),
placeholder: '0',
})
: renderPriceInput(__('设置价格', 'jio'), pay.price, (next) => patch({ price: next }), {
min: Number(bootstrap.priceMin || 0),
step: '0.01',
prefix: symbol,
placeholder: '0.00',
})}
{/* 会员价:只有后台开启「手动填写会员价」且存在会员等级时才需要作者填。 */}
{bootstrap.vipManual && (bootstrap.vip1Enabled || bootstrap.vip2Enabled) && (
<div className="jio-creator-vip-prices">
{bootstrap.vip1Enabled &&
(points
? renderPriceInput(
priceLabel(String(bootstrap.vip1Name || 'VIP1')),
String(pay.vip1Points),
(next) => patch({ vip1Points: toNumber(next) }),
{ step: '1', suffix: __('积分', 'jio'), placeholder: '0' }
)
: renderPriceInput(
priceLabel(String(bootstrap.vip1Name || 'VIP1')),
pay.vip1Price,
(next) => patch({ vip1Price: next }),
{ step: '0.01', prefix: symbol, placeholder: '0.00' }
))}
{bootstrap.vip2Enabled &&
(points
? renderPriceInput(
priceLabel(String(bootstrap.vip2Name || 'VIP2')),
String(pay.vip2Points),
(next) => patch({ vip2Points: toNumber(next) }),
{ step: '1', suffix: __('积分', 'jio'), placeholder: '0' }
)
: renderPriceInput(
priceLabel(String(bootstrap.vip2Name || 'VIP2')),
pay.vip2Price,
(next) => patch({ vip2Price: next }),
{ step: '0.01', prefix: symbol, placeholder: '0.00' }
))}
<p className="jio-creator-muted">
{__('会员价不能高于普通价,填 0 表示会员免费。', 'jio')}
</p>
</div>
)}
{canDownload && (
<div className="jio-creator-attachments">
<div className="jio-creator-attachments__head">
<div>
<h4>{__('付费下载', 'jio')}</h4>
<p className="jio-creator-muted">
{__('添加网盘链接或受保护附件,读者付费后才能下载。', 'jio')}
</p>
</div>
<div className="jio-creator-attachments__actions">
<button
type="button"
className="jio-creator-ghost"
onClick={() => setEditing({ row: null, index: -1 })}
>
<i className="fa fas fa-plus" aria-hidden="true" /> {__('添加资源', 'jio')}
</button>
{privateStorage && (
<label className="jio-creator-upload">
<input
ref={fileInput}
type="file"
onChange={(event) => void addUploaded(event.target.files?.[0])}
disabled={uploading}
/>
<span>{uploading ? __('上传中…', 'jio') : __('上传私有附件', 'jio')}</span>
</label>
)}
</div>
</div>
{!privateStorage && (
<p className="jio-creator-muted">
{__('私有附件存储尚未配置,只能添加外部下载地址。', 'jio')}
</p>
)}
{uploadError && <p className="jio-creator-error">{uploadError}</p>}
{uploads.length > 0 && (
<ul className="jio-creator-files">
{uploads.map((row, index) => (
<li key={`${row.assetKey || row.url}-${index}`}>
<button
type="button"
className="jio-creator-files__name"
onClick={() => setEditing({ row, index })}
title={__('编辑资源', 'jio')}
>
<i
className={
row.assetKey
? 'fa fas fa-lock'
: row.type === 'net_disk'
? 'fa fas fa-cloud'
: 'fa fas fa-link'
}
aria-hidden="true"
/>
{row.label || row.extractCode || row.fileName || row.url}
</button>
{row.sizeLabel && <span className="jio-creator-muted">{row.sizeLabel}</span>}
<button
type="button"
className="jio-creator-files__remove"
onClick={() =>
patch({ downloads: uploads.filter((_, position) => position !== index) })
}
aria-label={__('删除资源', 'jio')}
>
<i className="fa far fa-trash-alt" aria-hidden="true" />
</button>
</li>
))}
</ul>
)}
{privateUploads.length > 0 && (
<details className="jio-creator-reuse">
<summary>{__('从已上传资料中选择', 'jio')}</summary>
<ul>
{privateUploads
.filter((item) => !uploads.some((row) => row.assetKey === item.assetKey))
.map((item) => (
<li key={item.assetKey}>
<span>{item.fileName}</span>
<span className="jio-creator-muted">{item.sizeLabel}</span>
<button
type="button"
onClick={() =>
patch({
enabled: true,
downloads: [
...uploads,
{ ...item, type: 'protected', assetKey: item.assetKey },
],
})
}
>
{__('使用', 'jio')}
</button>
</li>
))}
</ul>
</details>
)}
</div>
)}
<p className="jio-creator-muted">
{__('请确保正文里已用「隐藏内容」标注了付费可见的片段。', 'jio')}
</p>
</div>
)}
{editing && (
<ResourceDialog
row={editing.row}
buttonClasses={buttonClasses}
onPickAttachment={onPickAttachment}
onSubmit={applyResource}
onClose={() => setEditing(null)}
/>
)}
</>
);
}
@@ -12,26 +12,8 @@ import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { apiUpload } from '../services/api';
type PayState = {
enabled: boolean;
modo: 'cash' | 'points';
price: string;
points: number;
vip1Price: string;
vip2Price: string;
payLimit: number;
downloads: Array<Record<string, any>>;
};
type Draft = {
postId: number;
title: string;
categories: number[];
tags: string[];
custom: Record<string, string[]>;
pay: PayState;
};
import type { DownloadRow, Draft } from '../draft';
import { PayPanel, type PayBootstrap } from './pay-panel';
type Capabilities = Record<string, boolean>;
@@ -87,9 +69,24 @@ export function PublishDrawer(props: Props) {
const [tagInput, setTagInput] = useState('');
const [uploadError, setUploadError] = useState('');
const [uploading, setUploading] = useState(false);
const fileInput = useRef<HTMLInputElement | null>(null);
const captchaSlot = useRef<HTMLDivElement | null>(null);
const payBootstrap: PayBootstrap = {
freeAllowed: !!bootstrap.pay?.freeAllowed,
vipManual: !!bootstrap.pay?.vipManual,
vip1Enabled: !!bootstrap.pay?.vip1Enabled,
vip2Enabled: !!bootstrap.pay?.vip2Enabled,
vip1Name: String(bootstrap.pay?.vip1Name || ''),
vip2Name: String(bootstrap.pay?.vip2Name || ''),
pointsEnabled: !!bootstrap.pay?.pointsEnabled,
downloadClasses: Array.isArray(bootstrap.pay?.downloadClasses) ? bootstrap.pay.downloadClasses : [],
currencySymbol: String(bootstrap.pay?.currencySymbol || ''),
priceMin: Number(bootstrap.pay?.priceMin || 0),
priceMax: Number(bootstrap.pay?.priceMax || 0),
pointsMin: Number(bootstrap.pay?.pointsMin || 0),
pointsMax: Number(bootstrap.pay?.pointsMax || 0),
};
// 人机验证由 PHP 渲染(脚本在页面加载时按 uid 注册实例),这里只把挂载点
// 移进抽屉,保证验证控件在用户提交时可见、可交互。
useEffect(() => {
@@ -132,18 +129,15 @@ export function PublishDrawer(props: Props) {
setTagInput('');
};
const uploadAttachment = async () => {
const file = fileInput.current?.files?.[0];
if (!file) {
return;
}
/** 上传一个受保护附件到私有存储,只保留 asset_key。 */
const uploadPrivate = async (file: File): Promise<DownloadRow | null> => {
setUploadError('');
setUploading(true);
try {
const response = await apiUpload(bootstrap.creator.privateUrl, file, nonce, {
post_id: String(draft.postId || 0),
});
const next = {
return {
type: 'protected',
assetKey: String(response.asset_key || ''),
fileName: String(response.name || file.name),
@@ -151,22 +145,35 @@ export function PublishDrawer(props: Props) {
sizeLabel: String(response.size_label || ''),
label: '',
};
setDraft({
pay: { ...draft.pay, enabled: true, downloads: [...draft.pay.downloads, next] },
});
if (fileInput.current) {
fileInput.current.value = '';
}
} catch (error: any) {
setUploadError(String(error?.message || __('资料上传失败', 'jio')));
return null;
} finally {
setUploading(false);
}
};
const removeDownload = (index: number) => {
setDraft({
pay: { ...draft.pay, downloads: draft.pay.downloads.filter((_, i) => i !== index) },
/**
* 从媒体库挑一个普通附件,把 URL 与附件 ID 回填到资源弹窗。
* 复用前台投稿的媒体选择器,避免再造一套上传 UI。
*/
const pickAttachment = (apply: (url: string, attachmentId: number) => void) => {
const media = (window as any).JioSubmitMedia;
if (!media || typeof media.open !== 'function') {
setUploadError(__('媒体选择器尚未加载,请刷新页面后重试。', 'jio'));
return;
}
media.open({
type: 'file',
multiple: false,
allowExternal: false,
onSelect: (selected: any) => {
const item = Array.isArray(selected) ? selected[0] : selected;
if (!item) {
return;
}
apply(String(item.url || ''), Number(item.attachment_id || item.id || 0));
},
});
};
@@ -276,150 +283,18 @@ export function PublishDrawer(props: Props) {
{capabilities.canPay && (
<section className="jio-creator-field">
<div className="jio-creator-switch">
<div>
<h3>{__('付费内容', 'jio')}</h3>
<p className="jio-creator-muted">{__('开启后,未购买读者只能看到免费部分。', 'jio')}</p>
</div>
<label className="jio-creator-toggle">
<input
type="checkbox"
checked={draft.pay.enabled}
onChange={(event) => setDraft({ pay: { ...draft.pay, enabled: event.target.checked } })}
/>
<span aria-hidden="true" />
</label>
</div>
{draft.pay.enabled && (
<div className="jio-creator-pay">
<div className="jio-creator-inline">
<label>
<input
type="radio"
checked={draft.pay.modo === 'cash'}
onChange={() => setDraft({ pay: { ...draft.pay, modo: 'cash' } })}
/>
<span>{__('现金', 'jio')}</span>
</label>
<label>
<input
type="radio"
checked={draft.pay.modo === 'points'}
onChange={() => setDraft({ pay: { ...draft.pay, modo: 'points' } })}
/>
<span>{__('积分', 'jio')}</span>
</label>
</div>
{draft.pay.modo === 'cash' ? (
<label className="jio-creator-input">
<span>{__('价格', 'jio')}</span>
<input
type="number"
min={Number(limits.priceMin || 0)}
step="0.01"
value={draft.pay.price}
onChange={(event) => setDraft({ pay: { ...draft.pay, price: event.target.value } })}
/>
</label>
) : (
<label className="jio-creator-input">
<span>{__('积分价格', 'jio')}</span>
<input
type="number"
min={0}
step="1"
value={draft.pay.points}
onChange={(event) =>
setDraft({ pay: { ...draft.pay, points: Number(event.target.value) || 0 } })
}
/>
</label>
)}
{capabilities.canPayDownload && (
<div className="jio-creator-attachments">
<div className="jio-creator-attachments__head">
<h4>{__('资料附件', 'jio')}</h4>
{privateStorage ? (
<label className="jio-creator-upload">
<input
ref={fileInput}
type="file"
onChange={() => void uploadAttachment()}
disabled={uploading}
/>
<span>{uploading ? __('上传中…', 'jio') : __('添加 PDF / ZIP', 'jio')}</span>
</label>
) : (
<p className="jio-creator-muted">
{__('私有附件存储尚未配置,暂时不能上传付费资料。', 'jio')}
</p>
)}
</div>
{uploadError && <p className="jio-creator-error">{uploadError}</p>}
{draft.pay.downloads.length > 0 && (
<ul className="jio-creator-files">
{draft.pay.downloads.map((row, index) => (
<li key={`${row.assetKey || row.url}-${index}`}>
<span className="jio-creator-files__name">
{row.assetKey ? (
<>
<i className="fa fas fa-lock" aria-hidden="true" />
{row.fileName || row.label || __('受保护资料', 'jio')}
</>
) : (
<>
<i className="fa fas fa-link" aria-hidden="true" />
{row.label || row.url}
</>
)}
</span>
{row.sizeLabel && <span className="jio-creator-muted">{row.sizeLabel}</span>}
<button type="button" onClick={() => removeDownload(index)} aria-label={__('移除', 'jio')}>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</li>
))}
</ul>
)}
{privateUploads.length > 0 && (
<details className="jio-creator-reuse">
<summary>{__('从已上传资料中选择', 'jio')}</summary>
<ul>
{privateUploads
.filter((item) => !draft.pay.downloads.some((row) => row.assetKey === item.assetKey))
.map((item) => (
<li key={item.assetKey}>
<span>{item.fileName}</span>
<span className="jio-creator-muted">{item.sizeLabel}</span>
<button
type="button"
onClick={() =>
setDraft({
pay: {
...draft.pay,
enabled: true,
downloads: [...draft.pay.downloads, { ...item, type: 'protected' }],
},
})
}
>
{__('使用', 'jio')}
</button>
</li>
))}
</ul>
</details>
)}
</div>
)}
</div>
)}
<PayPanel
pay={draft.pay}
onChange={(pay) => setDraft({ pay })}
canDownload={!!capabilities.canPayDownload}
privateStorage={privateStorage}
privateUploads={privateUploads}
bootstrap={payBootstrap}
onUploadPrivate={uploadPrivate}
uploading={uploading}
uploadError={uploadError}
onPickAttachment={pickAttachment}
/>
</section>
)}
@@ -35,6 +35,11 @@
box-shadow: 0 24px 70px rgba(20, 32, 28, 0.22);
}
/* 资源编辑器字段多,给一版更宽的面板。 */
.jio-creator-dialog__panel--wide {
width: min(560px, 100%);
}
.jio-creator-dialog__head {
display: flex;
align-items: center;
@@ -148,6 +153,86 @@
}
}
/* ---------- 付费资源编辑器 ---------- */
.jio-creator-dialog__hint {
color: #9aa2a0;
font-size: 12px;
line-height: 1.5;
}
.jio-creator-dialog__url {
display: flex;
gap: 8px;
input {
flex: 1;
min-width: 0;
}
.jio-creator-dialog__ghost {
flex: 0 0 auto;
height: 38px;
gap: 6px;
white-space: nowrap;
}
}
/* 按钮样式色块:用前台公开调色板(--jio-*)渲染,与发布后的下载按钮同一套色。 */
.jio-creator-swatches {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.jio-creator-swatch {
width: 30px;
height: 22px;
padding: 0;
border: 2px solid #fff;
border-radius: 6px;
background: #e3e7e5;
box-shadow: 0 0 0 1px #e3e7e5;
cursor: pointer;
&.b-theme {
background: var(--jc-accent);
}
&.c-red { background: var(--jio-rose, #f43f5e); }
&.c-yellow { background: var(--jio-yellow, #eab308); }
&.c-cyan { background: var(--jio-cyan, #06b6d4); }
&.c-blue { background: var(--jio-blue, #3b82f6); }
&.c-green { background: var(--jio-green, #10b981); }
&.c-purple { background: var(--jio-purple, #8b5cf6); }
&.b-red { background: var(--jio-red, #ef4444); }
&.b-yellow { background: var(--jio-yellow, #eab308); }
&.b-cyan { background: var(--jio-cyan, #06b6d4); }
&.b-blue { background: var(--jio-blue, #3b82f6); }
&.b-green { background: var(--jio-green, #10b981); }
&.b-purple { background: var(--jio-purple, #8b5cf6); }
&.jb-red { background: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
&.jb-pink { background: linear-gradient(135deg, var(--jio-grad-sunset-1, #f093fb) 10%, var(--jio-grad-sunset-2, #f5576c) 100%); }
&.jb-yellow { background: linear-gradient(135deg, var(--jio-grad-fire-2, #f9d423) 10%, var(--jio-orange, #f97316) 100%); }
&.jb-cyan { background: linear-gradient(135deg, var(--jio-grad-ocean-1, #4facfe) 10%, var(--jio-grad-ocean-2, #00f2fe) 100%); }
&.jb-blue { background: linear-gradient(135deg, #3dcabd 10%, #254bdd 100%); }
&.jb-green { background: linear-gradient(135deg, var(--jio-grad-forest-1, #43e97b) 10%, var(--jio-grad-forest-2, #38f9d7) 100%); }
&.jb-purple { background: linear-gradient(135deg, var(--jio-grad-night-1, #a18cd1) 10%, var(--jio-grad-night-2, #fbc2eb) 100%); }
&.jb-vip1 { background: linear-gradient(135deg, var(--jio-vip-gold-1, #f6d365) 10%, var(--jio-vip-gold-2, #fda085) 100%); }
&.jb-vip2 { background: linear-gradient(135deg, #f6d365 10%, #f43f5e 100%); }
&.is-active {
box-shadow: 0 0 0 2px var(--jc-accent);
}
&:focus-visible {
outline: 0;
box-shadow: 0 0 0 2px var(--jc-accent);
}
}
.jio-creator-dialog__foot {
display: flex;
justify-content: flex-end;
@@ -261,6 +261,60 @@
}
}
/* 价格输入:带货币 / 积分后缀,输入区仍保持右对齐的数字。 */
.jio-creator-price {
display: inline-flex;
align-items: center;
gap: 4px;
color: $ink-soft;
em {
font-size: 12px;
font-style: normal;
}
input {
width: 118px;
}
}
/* 现金 / 积分切换:分段控件,比原生 radio 更贴近前台投稿卡片。 */
.jio-creator-segment {
display: inline-flex;
gap: 4px;
margin-bottom: 4px;
padding: 3px;
border-radius: 9px;
background: #eef1f0;
button {
min-height: 30px;
padding: 0 14px;
border: 0;
border-radius: 7px;
background: transparent;
color: $ink-soft;
cursor: pointer;
font-size: 13px;
&:hover {
color: $accent-strong;
}
&.is-active {
background: #fff;
color: $accent-strong;
box-shadow: 0 1px 3px rgba(24, 39, 34, 0.1);
}
}
}
.jio-creator-vip-prices {
margin-top: 4px;
padding-top: 6px;
border-top: 1px dashed $line;
}
.jio-creator-cover {
position: relative;
overflow: hidden;
@@ -317,14 +371,25 @@
.jio-creator-attachments__head {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
h4 {
margin: 0;
margin: 0 0 2px;
font-size: 13px;
}
.jio-creator-muted {
margin: 0;
}
}
.jio-creator-attachments__actions {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
}
.jio-creator-upload {
@@ -361,7 +426,6 @@
}
button {
margin-left: auto;
border: 0;
background: transparent;
color: $ink-soft;
@@ -369,20 +433,45 @@
}
}
/* 资源名本身可点开编辑器,删除按钮单独占右侧。 */
.jio-creator-files__name {
display: inline-flex;
min-width: 0;
flex: 1;
align-items: center;
gap: 8px;
overflow: hidden;
padding: 0;
border: 0;
background: transparent;
color: $accent-strong;
cursor: pointer;
font: inherit;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
i {
flex: 0 0 auto;
color: $accent;
}
}
.jio-creator-files__remove {
flex: 0 0 auto;
margin-left: auto;
color: #9aa2a0 !important;
&:hover,
&:focus-visible {
color: $warn !important;
}
}
.jio-creator-reuse {
margin-top: 14px;
font-size: 13px;
+21 -20
View File
@@ -2,31 +2,24 @@
* 付费配置 → 请求体。
*
* 受保护附件只提交 asset_key,不提交任何文件路径或公开 URL;服务端会再次
* 校验附件归属并重新清洗。
* 校验附件归属并重新清洗。字段名与前台投稿表单(jio_pay[...])保持一致,
* 这样两条投稿路径写入的是同一份 pay meta。
*
* @package Jio
*/
type PayState = {
enabled: boolean;
modo: 'cash' | 'points';
price: string;
points: number;
vip1Price: string;
vip2Price: string;
payLimit: number;
downloads: Array<Record<string, any>>;
};
import type { DownloadRow, PayState } from '../draft';
export function normalizePayForRequest(pay: PayState) {
const downloads = (pay.downloads || [])
.map((row) => {
.map((row: DownloadRow) => {
const label = typeof row.label === 'string' ? row.label : '';
const assetKey = typeof row.assetKey === 'string' ? row.assetKey : '';
if (assetKey) {
return {
type: 'protected',
asset_key: assetKey,
label: typeof row.label === 'string' ? row.label : '',
label,
};
}
const url = typeof row.url === 'string' ? row.url.trim() : '';
@@ -34,23 +27,31 @@ export function normalizePayForRequest(pay: PayState) {
return null;
}
return {
type: 'direct',
type: row.type === 'net_disk' ? 'net_disk' : 'direct',
url,
label: typeof row.label === 'string' ? row.label : '',
label,
extract_code: typeof row.extractCode === 'string' ? row.extractCode : '',
copy_key: typeof row.copyKey === 'string' ? row.copyKey : '',
copy_val: typeof row.copyVal === 'string' ? row.copyVal : '',
class: typeof row.class === 'string' ? row.class : '',
attachment_id: Math.max(0, Number(row.attachmentId) || 0),
};
})
.filter(Boolean);
const points = pay.modo === 'points';
return {
enabled: !!pay.enabled,
fields: {
pay_type: pay.enabled ? (downloads.length > 0 ? 'download' : 'reading') : 'no',
pay_modo: pay.modo === 'points' ? 'points' : '0',
price: pay.modo === 'points' ? '0.00' : String(pay.price || '0.00'),
points_price: pay.modo === 'points' ? Math.max(0, Number(pay.points) || 0) : 0,
vip1_price: pay.modo === 'points' ? '0.00' : String(pay.vip1Price || '0.00'),
vip2_price: pay.modo === 'points' ? '0.00' : String(pay.vip2Price || '0.00'),
pay_modo: points ? 'points' : '0',
price: points ? '0.00' : String(pay.price || '0.00'),
points_price: points ? Math.max(0, Number(pay.points) || 0) : 0,
vip1_price: points ? '0.00' : String(pay.vip1Price || '0.00'),
vip2_price: points ? '0.00' : String(pay.vip2Price || '0.00'),
vip1_points: points ? Math.max(0, Number(pay.vip1Points) || 0) : 0,
vip2_points: points ? Math.max(0, Number(pay.vip2Points) || 0) : 0,
pay_limit: Number(pay.payLimit) || 0,
downloads,
},
+66 -9
View File
@@ -482,29 +482,36 @@ final class CreatorRest {
if (!is_array($row)) {
continue;
}
// 资源行里的展示字段与前台投稿的资源编辑器一一对应,缺一个就会在
// 审核应用后丢掉提取码 / 复制内容 / 按钮样式。
$display = [
'label' => (string) ($row['label'] ?? ''),
'extract_code' => (string) ($row['extract_code'] ?? ''),
'copy_key' => (string) ($row['copy_key'] ?? ''),
'copy_val' => (string) ($row['copy_val'] ?? ''),
'class' => (string) ($row['class'] ?? ''),
'attachment_id' => max(0, (int) ($row['attachment_id'] ?? 0)),
];
$key = PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? ''));
if ($key !== '') {
$asset = PrivateAssets::get($key);
if ($asset === null || $asset['owner_id'] !== get_current_user_id()) {
continue;
}
$downloads[] = [
$downloads[] = array_merge($display, [
'type' => 'protected',
'asset_key' => $key,
'file_name' => $asset['file_name'],
'size_bytes' => $asset['size_bytes'],
'label' => (string) ($row['label'] ?? ''),
];
]);
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url !== '') {
$downloads[] = [
'type' => 'direct',
'url' => $url,
'label' => (string) ($row['label'] ?? ''),
'extract_code' => (string) ($row['extract_code'] ?? ''),
];
$downloads[] = array_merge($display, [
'type' => (string) ($row['type'] ?? 'direct') === 'net_disk' ? 'net_disk' : 'direct',
'url' => $url,
]);
}
}
@@ -653,9 +660,17 @@ final class CreatorRest {
'label' => (string) ($row['label'] ?? ''),
'fileName' => (string) ($row['file_name'] ?? ''),
'sizeBytes' => (int) ($row['size_bytes'] ?? 0),
// 前台投稿的资源编辑器字段,缺一个就会在再次编辑时丢配置。
'extractCode' => (string) ($row['extract_code'] ?? ''),
'copyKey' => (string) ($row['copy_key'] ?? ''),
'copyVal' => (string) ($row['copy_val'] ?? ''),
'class' => (string) ($row['class'] ?? ''),
'attachmentId' => (int) ($row['attachment_id'] ?? 0),
];
}
$vip = $this->vip_labels();
return [
'enabled' => (bool) $enabled,
'canPay' => $front->user_can_cap('new_post_pay', $user),
@@ -665,9 +680,21 @@ final class CreatorRest {
'points' => (int) ($pay['points_price'] ?? 0),
'vip1Price' => (string) ($pay['vip1_price'] ?? '0.00'),
'vip2Price' => (string) ($pay['vip2_price'] ?? '0.00'),
'vip1Points' => (int) ($pay['vip1_points'] ?? 0),
'vip2Points' => (int) ($pay['vip2_points'] ?? 0),
'payLimit' => (int) ($pay['pay_limit'] ?? 0),
'downloads' => $downloads,
'freeAllowed' => !empty($settings['pay_free_s']),
// 会员价:手动填写或按折扣自动计算,决定前端是否渲染会员价输入框。
'vipManual' => !empty($settings['pay_vip_price_s']),
'vip1Enabled' => !empty($settings['pay_vip_1_enabled']),
'vip2Enabled' => !empty($settings['pay_vip_2_enabled']),
'vip1Name' => $vip[1],
'vip2Name' => $vip[2],
// 积分支付只有站点开启积分体系时才可选。
'pointsEnabled' => class_exists('\Jio\Settings\Settings') && \Jio\Settings\Settings::credits_enabled(),
'downloadClasses' => DomainModel::DOWNLOAD_BUTTON_CLASSES,
'currencySymbol' => $this->currency_symbol(),
'priceMin' => (int) ($settings['pay_price_min'] ?? 0),
'priceMax' => (int) ($settings['pay_price_max'] ?? 0),
'pointsMin' => (int) ($settings['pay_points_min'] ?? 0),
@@ -676,6 +703,36 @@ final class CreatorRest {
];
}
/**
* 会员等级名称:与前台付费卡片保持同一来源。
*
* @return array{1:string,2:string}
*/
private function vip_labels(): array {
$names = [1 => __('VIP1', 'jio'), 2 => __('VIP2', 'jio')];
if (!class_exists('\Jio\Settings\PaymentRuntime')) {
return $names;
}
$vip = \Jio\Settings\PaymentRuntime::get_payment_vip_settings();
foreach ([1, 2] as $level) {
$name = trim((string) ($vip['levels'][$level]['name'] ?? ''));
if ($name !== '') {
$names[$level] = $name;
}
}
return $names;
}
/** 站点货币符号,用于价格输入框的前缀。 */
private function currency_symbol(): string {
if (!class_exists('\Jio\Settings\PaymentRuntime')) {
return '¥';
}
$paid = \Jio\Settings\PaymentRuntime::get_paid_content_settings();
$symbol = trim((string) ($paid['display']['currency_symbol'] ?? ''));
return $symbol !== '' ? $symbol : '¥';
}
/**
* @return array<string, bool>
*/
@@ -144,7 +144,8 @@ test('studio app uses TinyMCE while preserving autosave, recovery, and publishin
assert.match(api, /'X-WP-Nonce'/);
assert.match(drawer, /canPay/);
assert.match(drawer, /canPayDownload/);
assert.match(drawer, /私有附件存储尚未配置/);
assert.match(drawer, /<PayPanel/);
assert.match(read('assets/creator-studio/src/panels/pay-panel.tsx'), /私有附件存储尚未配置/);
assert.match(drawer, /onSubmit\(false\)/);
assert.match(drawer, /立即发布/);
});
@@ -454,3 +455,75 @@ test('character count lives in the fixed bottom bar, not a reading-time footer',
assert.doesNotMatch(appSource, /分钟阅读/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /字数/);
});
// 发布抽屉的付费配置必须与前台投稿的「付费内容」卡片等价。
test('publish drawer pay settings match the front-submit pay card', () => {
const payPanel = read('assets/creator-studio/src/panels/pay-panel.tsx');
const resource = read('assets/creator-studio/src/components/resource-dialog.tsx');
const payUtil = read('assets/creator-studio/src/utils/pay.ts');
const draft = read('assets/creator-studio/src/draft.ts');
const rest = read('src/Core/CreatorRest.php');
// 现金 / 积分两种计价方式,且积分只在站点开启积分体系时出现。
assert.match(payPanel, /现金支付/);
assert.match(payPanel, /积分支付/);
assert.match(payPanel, /bootstrap\.pointsEnabled/);
assert.match(rest, /'pointsEnabled'\s*=>/);
// 会员价:手动填写模式与自动折扣模式由服务端决定。
assert.match(payPanel, /bootstrap\.vipManual/);
assert.match(payPanel, /bootstrap\.vip1Enabled/);
assert.match(payPanel, /bootstrap\.vip2Enabled/);
assert.match(payPanel, /vip1Points/);
assert.match(payPanel, /vip2Points/);
assert.match(rest, /'vipManual'\s*=>/);
assert.match(rest, /'vip1Name'\s*=>/);
assert.match(draft, /vip1Points: Number\(pay\.vip1Points/);
// 请求体带齐服务端会校验的会员价字段。
assert.match(payUtil, /vip1_points:/);
assert.match(payUtil, /vip2_points:/);
assert.match(payUtil, /vip1_price:/);
assert.match(payUtil, /vip2_price:/);
// 资源编辑器:与前台投稿一致的七个字段。
for (const field of ['extract_code', 'copy_key', 'copy_val', 'class', 'attachment_id']) {
assert.ok(payUtil.includes(field), `missing download field in request: ${field}`);
}
for (const label of ['下载地址', '资源备注', '复制名称', '复制内容', '自定义按钮文案', '按钮样式']) {
assert.ok(resource.includes(label), `missing resource field: ${label}`);
}
assert.match(resource, /上传附件/);
assert.match(resource, /提取码/);
assert.match(resource, /onPaste/);
assert.match(payPanel, /添加资源/);
assert.match(payPanel, /<ResourceDialog/);
// 按钮样式色块与保存端的白名单同源。
assert.match(rest, /'downloadClasses'\s*=>\s*DomainModel::DOWNLOAD_BUTTON_CLASSES/);
assert.match(payPanel, /bootstrap\.downloadClasses/);
// 受保护附件仍然只提交 asset_key。
assert.match(payUtil, /asset_key: assetKey/);
assert.doesNotMatch(payUtil, /wp-content|file_path/);
});
// 审核 / 发布路径要把资源的展示字段一并带进快照,否则会丢提取码与按钮样式。
test('creator snapshot keeps every download display field', () => {
const rest = read('src/Core/CreatorRest.php');
const snapshot = rest.slice(
rest.indexOf('private function snapshot_from_payload'),
rest.indexOf('private function claim_private_assets')
);
assert.match(snapshot, /'extract_code'\s*=>/);
assert.match(snapshot, /'copy_key'\s*=>/);
assert.match(snapshot, /'copy_val'\s*=>/);
assert.match(snapshot, /'class'\s*=>/);
assert.match(snapshot, /'attachment_id'\s*=>/);
assert.match(snapshot, /'net_disk'/);
// bootstrap 回读时字段名要与前端模型一致。
assert.match(rest, /'extractCode'\s*=>/);
assert.match(rest, /'copyKey'\s*=>/);
assert.match(rest, /'copyVal'\s*=>/);
});