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:
1 parent
c5bdbc0457
commit
0e4e4670dd
12 files changed
+1022
-224
No files matched your search
@@ -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 @@
|
||||
.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
|
||||
@@ -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,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");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,"&").replace(/</g,"<").replace(/>/g,">").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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),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,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");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,"&").replace(/</g,"<").replace(/>/g,">").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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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*=>/);
|
||||
});
|
||||
Reference in new issue
Block a user