feat(creator-studio): centered paper layout and scheduled publishing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
384d2b7aa0
commit
7bdf7cf018
20 files changed
+549
-308
No files matched your search
@@ -10,14 +10,14 @@ export const submitSchema = {
|
||||
post_article_custom_filter_s: true,
|
||||
post_article_tag: true,
|
||||
post_article_user: 1,
|
||||
post_article_title_strlen_limit: { min: 5, max: 30 },
|
||||
post_article_title_strlen_limit: { min: 5, max: 100 },
|
||||
post_article_pay_free_s: true,
|
||||
post_article_pay_price_limit: { price: { min: 0, max: 99999 }, points: { min: 0, max: 0 } },
|
||||
post_article_pay_vip_price_s: true,
|
||||
post_article_pay_vip_1_discount: 100,
|
||||
post_article_pay_vip_2_discount: 100,
|
||||
post_t_placeholder: '请输入标题',
|
||||
post_c_placeholder: '请输入内容',
|
||||
post_t_placeholder: '请输入标题(最多 100 个字)',
|
||||
post_c_placeholder: '请输入正文',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
@@ -44,7 +44,7 @@ export const submitSchema = {
|
||||
},
|
||||
{ key: 'post_article_tag', label: '允许插入标签', type: 'switch', default: true, dependsOn: { key: 'post_article_s', value: true }, description: '前台投稿时候允许设置文章标签' },
|
||||
{ key: 'post_article_user', label: '免登陆投稿发布用户', type: 'user_select', default: 1, dependsOn: { key: 'post_article_s', value: true }, description: '当选择投稿权限为无需登录就能投稿时,投稿文章的用户' },
|
||||
{ key: 'post_article_title_strlen_limit', label: '标题字数限制', type: 'between_number', default: { min: 5, max: 30 }, unit: '字', description: '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)' },
|
||||
{ key: 'post_article_title_strlen_limit', label: '标题字数限制', type: 'between_number', default: { min: 5, max: 100 }, unit: '字', description: '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)' },
|
||||
{ key: 'post_article_pay_free_s', label: '允许设置为免费资源', type: 'switch', default: true, dependsOn: { key: 'post_article_s', value: true }, description: '付费内容金额允许设置为 0' },
|
||||
{
|
||||
key: 'post_article_pay_price_limit',
|
||||
|
||||
@@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '7498c793a760249be02b');
|
||||
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'ccaa65aec8e25f38d35c');
|
||||
@@ -1 +1 @@
|
||||
.jio-page-submit{background:#fafbfa}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:#243330;--jc-ink-soft:#5c6b66;--jc-paper:#fafbfa;--jc-line:#e2e9e5;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:60px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;height:34px;justify-content:center;min-width:32px;padding:0 8px}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#edf3f0;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-labeled span{white-space:nowrap}.jio-creator__format-select{background:transparent;border:1px solid transparent;border-radius:7px;color:var(--jc-ink-soft);flex:0 0 auto;font:inherit;font-size:13px;height:34px;padding:0 8px;width:94px}.jio-creator__format-select:focus-visible,.jio-creator__format-select:hover{background:#fff;border-color:var(--jc-line);outline:0}.jio-creator__format-select:disabled{color:#aebbb5}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:var(--jc-warn)}.jio-creator__btn,.jio-creator__ghost,.jio-creator__primary{align-items:center;border:1px solid transparent;border-radius:7px;cursor:pointer;display:inline-flex;font-size:13px;justify-content:center;min-height:34px;padding:0 13px;transition:background .18s ease,border-color .18s ease,color .18s ease;white-space:nowrap}.jio-creator__ghost{background:transparent;border-color:var(--jc-line)}.jio-creator__ghost:focus-visible,.jio-creator__ghost:hover{border-color:var(--jc-accent);color:var(--jc-accent)}.jio-creator__primary{background:var(--jc-accent);color:#fff}.jio-creator__primary:focus-visible,.jio-creator__primary:hover{backLine truncated
|
||||
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:60px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;height:34px;justify-content:center;min-width:32px;padding:0 8px}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#edf3f0;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-labeled span{white-space:nowrap}.jio-creator__format-select{background:transparent;border:1px solid transparent;border-radius:7px;color:var(--jc-ink-soft);flex:0 0 auto;font:inherit;font-size:13px;height:34px;padding:0 8px;width:94px}.jio-creator__format-select:focus-visible,.jio-creator__format-select:hover{background:#fff;border-color:var(--jc-line);outline:0}.jio-creator__format-select:disabled{color:#aebbb5}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:var(--jc-warn)}.jio-creator__btn,.jio-creator__ghost,.jio-creator__primary{align-items:center;border:1px solid transparent;border-radius:7px;cursor:pointer;display:inline-flex;font-size:13px;justify-content:center;min-height:34px;padding:0 13px;transition:background .18s ease,border-color .18s ease,color .18s ease;white-space:nowrap}.jio-creator__ghost{background:transparent;border-color:var(--jc-line)}.jio-creator__ghost:focus-visible,.jio-creator__ghost:hover{border-color:var(--jc-accent);color:var(--jc-accent)}.jio-creator__primary{background:var(--jc-accent);color:#fff}.jio-creator__primary:focus-visible,.jio-creator__primary:hover{backLine truncated
|
||||
@@ -1 +1,3 @@
|
||||
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function r(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),r=new Error(String(a));return r.status=t,r.code=e&&e.code?String(e.code):"",r.data=e,r}async function n(e){try{return await e.json()}catch(e){return null}}async function o(e,t,a){const o=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await n(o);if(!o.ok)throw r(i,o.status);return i}async function i(e,t,a,o={}){const i=new FormData;i.append("file",t),Object.entries(o).forEach(([e,t])=>i.append(e,t));const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:i}),c=await n(l);if(!l.ok)throw r(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(r){const{draft:n,setDraft:o,capabilities:s,limits:u,taxonomy:m,isPublishedEdit:d,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:y,saving:j,captcha:E}=r,[g,h]=(0,t.useState)(""),[v,N]=(0,t.useState)(""),[w,k]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null),x=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=x.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const I=(0,t.useMemo)(()=>(n.title||"").trim().length,[n.title])>=Number(u.titleMin||1),M=()=>{const e=g.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);o({tags:Array.from(new Set([...n.tags,...t]))}),h("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},d?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},d&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("封面","jio")),n.featured?.data?.url?(0,e.createElement)("div",{className:"jio-creator-cover"},(0,e.createElement)("img",{src:String(n.featured.data.url),alt:""}),(0,e.createElement)("button",{type:"button",onClick:()=>o({featured:{type:"",data:{}}}),"aria-label":(0,a.__)("移除封面","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))):s.canUploadImage?(0,e.createElement)("label",{className:"jio-creator-cover-empty"},(0,e.createElement)("input",{ref:C,type:"file",accept:"image/*",onChange:()=>{(async()=>{const e=C.current?.files?.[0];if(e&&l.media?.uploadUrl?.image){N(""),k(!0);try{const t=await i(l.media.uploadUrl.image,e,c),r=Number(t.attachment_id||t.id||0),n=String(t.url||"");if(!r||!n)throw new Error((0,a.__)("上传后无法读取图片。","jio"));o({featured:{type:"image",mediaId:r,data:{id:r,url:n}}})}catch(e){N(String(e?.message||(0,a.__)("上传失败","jio")))}finally{k(!1)}}})()},disabled:w}),(0,e.createElement)("i",{className:"fa fas fa-plus","aria-hidden":"true"}),(0,e.createElement)("span",null,w?(0,a.__)("上传中…","jio"):(0,a.__)("上传封面","jio"))):(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("当前账号不能设置封面。","jio"))),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===m.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),m.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(n.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:n.categories.includes(t.id),onChange:()=>(e=>{const t=n.categories.includes(e);o({categories:t?n.categories.filter(t=>t!==e):[...n.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),m.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},n.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createLine truncated
|
||||
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function r(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),r=new Error(String(a));return r.status=t,r.code=e&&e.code?String(e.code):"",r.data=e,r}async function n(e){try{return await e.json()}catch(e){return null}}async function o(e,t,a){const o=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await n(o);if(!o.ok)throw r(i,o.status);return i}async function i(e,t,a,o={}){const i=new FormData;i.append("file",t),Object.entries(o).forEach(([e,t])=>i.append(e,t));const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:i}),c=await n(l);if(!l.ok)throw r(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(r){const{draft:n,setDraft:o,capabilities:s,limits:u,taxonomy:m,isPublishedEdit:d,onClose:p,onSaveDraft:_,onSubmit:b,privateStorage:f,privateUploads:g,saving:j,captcha:h}=r,[E,y]=(0,t.useState)(""),[v,N]=(0,t.useState)(""),[w,k]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(n.title||"").trim().length,[n.title])>=Number(u.titleMin||1),I=()=>{const e=E.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);o({tags:Array.from(new Set([...n.tags,...t]))}),y("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},d?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},d&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===m.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),m.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(n.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:n.categories.includes(t.id),onChange:()=>(e=>{const t=n.categories.includes(e);o({categories:t?n.categories.filter(t=>t!==e):[...n.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),m.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},n.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>o({tags:n.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:E,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>y(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),I())}}),(0,e.createElement)("button",{type:"button",onClick:I},(0,a.__)("添加","jio")))),m.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const r=n.custom[t.key]||[],i=r.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(i?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:i,onChange:()=>o({custom:{...n.custom,[t.key]:i?r.filter(e=>e!==a.key):[...r,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
|
||||
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}function f({author:t}){const r="string"==typeof t?.name?t.name.trim():"";if(""===r)return null;const n="string"==typeof t?.avatar?t.avatar:"",o="string"==typeof t?.meta?t.meta:"";return(0,e.createElement)("div",{className:"jio-creator__author"},""!==n?(0,e.createElement)("img",{className:"jio-creator__author-avatar",src:n,alt:"",width:24,height:24}):(0,e.createElement)("span",{className:"jio-creator__author-avatar is-placeholder","aria-hidden":"true"},r.slice(0,1)),(0,e.createElement)("span",{className:"jio-creator__author-name"},r),""!==o&&(0,e.createElement)("span",{className:"jio-creator__author-meta"},o),(0,e.createElement)("span",{className:"jio-creator__author-hint"},(0,a.__)("原创内容","jio")))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function j({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function h(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const E=window.jioCreatorStudio||{},y=E.nonce||"",v=`jio-studio-${E.sourceId||0}`,N="jio-creator-content";function w(){const e=E.pay||{},t=E.categories&&E.categories.selected||[],a=E.tags&&E.tags.items||[];return{postId:Number(E.sourceId||0),versionId:Number(E.versionId||0),rev:Number(E.versionRev||0),title:String(E.title||""),content:String(E.content||""),schedule:{enabled:!1,at:""},categories:t.map(Number),tags:a.slice(),custom:E.custom||{},featured:E.featured||{type:"",data:{}},pay:{enabled:!!e.enabled,modo:"points"===e.payModo?"points":"cash",price:String(e.price||"0.00"),points:Number(e.points||0),vip1Price:String(e.vip1Price||"0.00"),vip2Price:String(e.vip2Price||"0.00"),payLimit:Number(e.payLimit||0),downloads:Array.isArray(e.downloads)?e.downloads:[]}}}function k(e){return e.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}function C(e){const t=String(e||"").trim();if(!t)return"";try{const e=new URL(t,window.location.href);return"http:"===e.protocol||"https:"===e.protocol?e.href:""}catch(e){return""}}function S(e){const t=document.createElement("div");return t.innerHTML=e,(t.textContent||"").replace(/\s+/g,"")}function x(){const[r,n]=(0,t.useState)(w),i=(0,t.useRef)(r);i.current=r;const l=(0,t.useRef)(null),c=(0,t.useRef)(null),m=(0,t.useRef)(null),d=(0,t.useRef)(!1),p=(0,t.useRef)(()=>{}),[_,g]=(0,t.useState)(!1),[x,I]=(0,t.useState)({canUndo:!1,canRedo:!1}),[M,L]=(0,t.useState)("p"),[R,D]=(0,t.useState)(()=>({state:E.sourceId||E.versionId||E.content?"saved":"ready",at:""})),[P,U]=(0,t.useState)(!1),[$,F]=(0,t.useState)(null),[T,z]=(0,t.useState)(null),K=(0,t.useRef)(!1),O=(0,t.useRef)(0),A=E.capabilities||{},B={titleMin:Number(E.limits?.titleMin||1),titleMax:Number(E.limits?.titleMax||30),titlePlaceholder:String(E.limits?.titlePlaceholder||(0,a.__)("请输入标题","jio")),priceMin:Number(E.pay?.priceMin||0),priceMax:Number(E.pay?.priceMax||0),pointsMin:Number(E.pay?.pointsMin||0),pointsMax:Number(E.pay?.pointsMax||0)},J=!!E.isPublished,q=(0,t.useCallback)(e=>{K.current=!0,O.current+=1;const t={...i.current,...e};i.current=t,n(t)},[]),V=(0,t.useCallback)(e=>{if(e===i.current.content)return;K.current=!0,O.current+=1;const t={...i.current,content:e};i.current=t,n(t)},[]),H=(0,t.useCallback)(e=>{const t=e?.undoManager;t&&I({canUndo:"function"==typeof t.hasUndo&&t.hasUndo(),canRedo:"function"==typeof t.hasRedo&&t.hasRedo()})},[]),W=(0,t.useCallback)(e=>{const t=e?.selection?.getNode(),a=t?e.dom.getParent(t,"p,h1,h2,h3,blockquote")||t:null,r=a?.nodeName?.toLowerCase()||"p";L(["h1","h2","h3","blockquote"].includes(r)?r:"p")},[]),X=(0,t.useMemo)(()=>Array.from(S(r.content)).length,[r.content]),Y=(0,t.useCallback)((e={})=>{const t=i.currenLine truncated
|
||||
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(B.titleMax)||100),spellCheck:!1,maxLength:Number(B.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:N,defaultValue:r.content,onChange:e=>V(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:r.featured,canEditCover:!!A.canUploadImage,canSchedule:!!A.canSchedule,scheduleEnabled:!!r.schedule.enabled,scheduleAt:r.schedule.at,onFeaturedChange:e=>q({featured:e}),onScheduleChange:(e,t)=>q({schedule:{enabled:e,at:t}})}),(0,e.createElement)("footer",{className:"jio-creator__footer"},(0,e.createElement)("span",null,(0,a.__)("字数","jio"),":",X),(0,e.createElement)("span",null,(0,a.__)("约","jio")," ",Math.max(1,Math.round(X/400))," ",(0,a.__)("分钟阅读","jio"))),P&&(0,e.createElement)(s,{draft:r,setDraft:q,capabilities:A,limits:B,taxonomy:{categories:E.categories&&E.categories.items||[],tags:!!E.tags?.enabled,customDefs:E.customDefs||[]},isPublishedEdit:J,onClose:()=>U(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{G(e)},privateStorage:!!E.privateStorage,privateUploads:E.privateUploads||[],captcha:!!E.captcha?.enabled,saving:"saving"===R.state||"submitting"===R.state}))}function I(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(x,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()})();
|
||||
@@ -63,10 +63,11 @@ const mockBootstrap = {
|
||||
canPay: true,
|
||||
canPayDownload: true,
|
||||
canPublishNow: true,
|
||||
canSchedule: true,
|
||||
canEdit: true,
|
||||
isLoggedIn: true,
|
||||
},
|
||||
limits: { titleMin: 5, titleMax: 30, titlePlaceholder: '请输入标题', contentPlaceholder: '开始写作…' },
|
||||
limits: { titleMin: 5, titleMax: 100, titlePlaceholder: '请输入标题', contentPlaceholder: '请输入正文' },
|
||||
author: { name: '预览作者', avatar: '', meta: '' },
|
||||
privateUploads: [],
|
||||
privateStorage: false,
|
||||
@@ -82,6 +83,7 @@ const mockBootstrap = {
|
||||
bootstrapUrl: '#',
|
||||
},
|
||||
nonce: 'preview',
|
||||
timezone: 'Asia/Shanghai',
|
||||
};
|
||||
|
||||
// 换成模拟数据。WP 的 wp_add_inline_script 会在结尾追加 sourceURL 注释,
|
||||
|
||||
@@ -57,7 +57,7 @@ const PREFIX = process.argv[3] || 'C:/Users/Xing/Desktop/WP-Date/data/wordpress/
|
||||
return {
|
||||
bar: pick('.jio-creator__bar'),
|
||||
title: pick('.jio-creator__title textarea'),
|
||||
excerpt: pick('.jio-creator__excerpt textarea'),
|
||||
settings: pick('.jio-creator__settings'),
|
||||
canvas: pick('.jio-creator__canvas'),
|
||||
toolbar: pick('.jio-creator__toolbar-tools'),
|
||||
editor: pick('.jio-creator__editor .mce-edit-area'),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 封面预览条(公众号式)。
|
||||
*
|
||||
* 位于标题上方一行:有封面时显示缩略图,点击回到发布抽屉的封面区;
|
||||
* 无封面时给一个低调的入口。封面能力由服务端 bootstrap 决定,前端不放大权限。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
|
||||
type Props = {
|
||||
featured: Featured;
|
||||
canEdit: boolean;
|
||||
onOpen: () => void;
|
||||
onClear: () => void;
|
||||
};
|
||||
|
||||
export function CoverStrip({ featured, canEdit, onOpen, onClear }: Props) {
|
||||
const url = typeof featured?.data?.url === 'string' ? featured.data.url : '';
|
||||
|
||||
if (!canEdit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<button type="button" className="jio-creator__cover is-empty" onClick={onOpen}>
|
||||
<i className="fa far fa-image" aria-hidden="true" />
|
||||
<span>{__('添加封面', 'jio')}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jio-creator__cover">
|
||||
<button type="button" className="jio-creator__cover-thumb" onClick={onOpen}>
|
||||
<img src={url} alt="" />
|
||||
<span className="jio-creator__cover-edit">
|
||||
<i className="fa fas fa-pen" aria-hidden="true" />
|
||||
{__('更换封面', 'jio')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-clear"
|
||||
onClick={onClear}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 发布设置卡片(位于正文纸张下方)。
|
||||
*
|
||||
* 只承载与"这篇会被怎样发布"有关的低频选项:封面与定时发布。
|
||||
* 分类、标签、付费等配置仍在发布抽屉里;服务端 bootstrap 决定每项是否可用。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { useMemo, useRef, useState } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiUpload } from '../services/api';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
|
||||
type Props = {
|
||||
featured: Featured;
|
||||
canEditCover: boolean;
|
||||
canSchedule: boolean;
|
||||
scheduleEnabled: boolean;
|
||||
scheduleAt: string;
|
||||
onFeaturedChange: (featured: Featured) => void;
|
||||
onScheduleChange: (enabled: boolean, at: string) => void;
|
||||
};
|
||||
|
||||
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
||||
const nonce: string = bootstrap.nonce || '';
|
||||
|
||||
const pad = (value: number): string => String(value).padStart(2, '0');
|
||||
|
||||
function toLocalInput(date: Date): string {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function defaultScheduleTime(): string {
|
||||
// 默认给一小时后,避免用户点开开关就看到一个"已经过去"的时间。
|
||||
return toLocalInput(new Date(Date.now() + 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
export function PublishSettings({
|
||||
featured,
|
||||
canEditCover,
|
||||
canSchedule,
|
||||
scheduleEnabled,
|
||||
scheduleAt,
|
||||
onFeaturedChange,
|
||||
onScheduleChange,
|
||||
}: Props) {
|
||||
const coverInput = useRef<HTMLInputElement | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const minSchedule = useMemo(() => toLocalInput(new Date(Date.now() + 5 * 60 * 1000)), []);
|
||||
const coverUrl = typeof featured?.data?.url === 'string' ? featured.data.url : '';
|
||||
|
||||
const uploadCover = async () => {
|
||||
const file = coverInput.current?.files?.[0];
|
||||
const uploadUrl = bootstrap.media?.uploadUrl?.image;
|
||||
if (!file || !uploadUrl) {
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setUploading(true);
|
||||
try {
|
||||
const response = await apiUpload(uploadUrl, file, nonce);
|
||||
const id = Number(response.attachment_id || response.id || 0);
|
||||
const url = String(response.url || '');
|
||||
if (!id || !url) {
|
||||
throw new Error(__('上传后无法读取图片。', 'jio'));
|
||||
}
|
||||
onFeaturedChange({ type: 'image', mediaId: id, data: { id, url } });
|
||||
} catch (uploadError: any) {
|
||||
setError(String(uploadError?.message || __('上传失败', 'jio')));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (coverInput.current) {
|
||||
coverInput.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="jio-creator__settings" aria-label={__('发布设置', 'jio')}>
|
||||
<h2 className="jio-creator__settings-title">{__('发布设置', 'jio')}</h2>
|
||||
|
||||
{canEditCover && (
|
||||
<div className="jio-creator__setting-row">
|
||||
<div className="jio-creator__setting-copy">
|
||||
<span className="jio-creator__setting-label">{__('添加封面', 'jio')}</span>
|
||||
<p className="jio-creator__setting-help">{__('用于列表卡片、分享卡片;不设置时自动使用正文首图。', 'jio')}</p>
|
||||
</div>
|
||||
<div className="jio-creator__cover-control">
|
||||
{coverUrl ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-preview"
|
||||
onClick={() => coverInput.current?.click()}
|
||||
aria-label={__('更换封面', 'jio')}
|
||||
>
|
||||
<img src={coverUrl} alt="" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-clear"
|
||||
onClick={() => onFeaturedChange({ type: '', data: {} })}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="jio-creator__cover-add"
|
||||
onClick={() => coverInput.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<i className="fa fas fa-plus" aria-hidden="true" />
|
||||
<span>{uploading ? __('上传中…', 'jio') : __('添加封面', 'jio')}</span>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={coverInput}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="jio-creator__cover-input"
|
||||
onChange={() => void uploadCover()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="jio-creator-error">{error}</p>}
|
||||
|
||||
<div className="jio-creator__setting-row">
|
||||
<div className="jio-creator__setting-copy">
|
||||
<span className="jio-creator__setting-label">{__('定时发布', 'jio')}</span>
|
||||
<p className="jio-creator__setting-help">
|
||||
{canSchedule
|
||||
? __('开启后到点自动发布,发布前不会出现在文章列表。', 'jio')
|
||||
: __('当前账号提交后需要审核,审核通过后才会发布。', 'jio')}
|
||||
</p>
|
||||
</div>
|
||||
<label className="jio-creator-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={scheduleEnabled}
|
||||
disabled={!canSchedule}
|
||||
onChange={(event) => onScheduleChange(event.target.checked, scheduleAt || defaultScheduleTime())}
|
||||
/>
|
||||
<span aria-hidden="true" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{canSchedule && scheduleEnabled && (
|
||||
<div className="jio-creator__schedule">
|
||||
<label>
|
||||
<span>{__('发布时间', 'jio')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduleAt}
|
||||
min={minSchedule}
|
||||
onChange={(event) => onScheduleChange(true, event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<p className="jio-creator-muted">
|
||||
{sprintf(
|
||||
/* translators: %s: site timezone */
|
||||
__('按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。', 'jio'),
|
||||
String(bootstrap.timezone || 'UTC')
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
*/
|
||||
|
||||
import { createRoot, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiPost } from './services/api';
|
||||
import { PublishDrawer } from './panels/publish-drawer';
|
||||
import { CoverStrip } from './components/cover-strip';
|
||||
import { AuthorRow } from './components/author-row';
|
||||
import { EditorToolbar } from './components/editor-toolbar';
|
||||
import { PublishSettings } from './components/publish-settings';
|
||||
import { AuthorRow } from './components/author-row';
|
||||
import { SaveState, type SaveStateValue } from './components/save-state';
|
||||
import { useAutosave } from './hooks/use-autosave';
|
||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||
@@ -48,13 +48,15 @@ type Limits = {
|
||||
pointsMax?: number;
|
||||
};
|
||||
|
||||
type Schedule = { enabled: boolean; at: string };
|
||||
|
||||
type Draft = {
|
||||
postId: number;
|
||||
versionId: number;
|
||||
rev: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
schedule: Schedule;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
@@ -71,8 +73,8 @@ function initialDraft(): Draft {
|
||||
versionId: Number(bootstrap.versionId || 0),
|
||||
rev: Number(bootstrap.versionRev || 0),
|
||||
title: String(bootstrap.title || ''),
|
||||
excerpt: String(bootstrap.excerpt || ''),
|
||||
content: String(bootstrap.content || ''),
|
||||
schedule: { enabled: false, at: '' },
|
||||
categories: selected.map(Number),
|
||||
tags: tags.slice(),
|
||||
custom: bootstrap.custom || {},
|
||||
@@ -191,12 +193,15 @@ function Studio(): JSX.Element {
|
||||
version_id: current.versionId,
|
||||
rev: current.rev,
|
||||
title: current.title,
|
||||
excerpt: current.excerpt,
|
||||
content: current.content,
|
||||
categories: current.categories,
|
||||
tags: current.tags,
|
||||
custom: current.custom,
|
||||
featured: current.featured,
|
||||
schedule: {
|
||||
enabled: !!current.schedule.enabled,
|
||||
at: current.schedule.enabled ? current.schedule.at : '',
|
||||
},
|
||||
pay: normalizePayForRequest(current.pay),
|
||||
...extra,
|
||||
};
|
||||
@@ -613,12 +618,7 @@ function Studio(): JSX.Element {
|
||||
)}
|
||||
|
||||
<main className="jio-creator__canvas">
|
||||
<CoverStrip
|
||||
featured={draft.featured}
|
||||
canEdit={!!capabilities.canUploadImage}
|
||||
onOpen={() => setDrawerOpen(true)}
|
||||
onClear={() => patchDraft({ featured: { type: '', data: {} } })}
|
||||
/>
|
||||
<AuthorRow author={bootstrap.author || {}} />
|
||||
|
||||
<label className="jio-creator__title">
|
||||
<span className="screen-reader-text">{__('文章标题', 'jio')}</span>
|
||||
@@ -626,33 +626,36 @@ function Studio(): JSX.Element {
|
||||
value={draft.title}
|
||||
onChange={(event) => patchDraft({ title: event.target.value })}
|
||||
rows={1}
|
||||
placeholder={limits.titlePlaceholder || __('请输入标题', 'jio')}
|
||||
maxLength={(Number(limits.titleMax) || 30) * 4}
|
||||
placeholder={sprintf(
|
||||
/* translators: %d: max title length */
|
||||
__('请输入标题(最多 %d 个字)', 'jio'),
|
||||
Number(limits.titleMax) || 100
|
||||
)}
|
||||
spellCheck={false}
|
||||
maxLength={Number(limits.titleMax) || 100}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="jio-creator__excerpt">
|
||||
<span>{__('摘要(可选)', 'jio')}</span>
|
||||
<textarea
|
||||
value={draft.excerpt}
|
||||
onChange={(event) => patchDraft({ excerpt: event.target.value })}
|
||||
rows={2}
|
||||
placeholder={__('一句话概括这篇文章', 'jio')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<AuthorRow author={bootstrap.author || {}} />
|
||||
|
||||
<div className="jio-creator__editor" data-editor-ready={editorReady ? 'true' : 'false'}>
|
||||
<textarea id={EDITOR_ID} defaultValue={draft.content} onChange={(event) => updateContent(event.target.value)} aria-label={__('文章正文', 'jio')} />
|
||||
</div>
|
||||
|
||||
<footer className="jio-creator__footer">
|
||||
<span>{__('字数', 'jio')}:{charCount}</span>
|
||||
<span>{__('约', 'jio')} {Math.max(1, Math.round(charCount / 400))} {__('分钟阅读', 'jio')}</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<PublishSettings
|
||||
featured={draft.featured}
|
||||
canEditCover={!!capabilities.canUploadImage}
|
||||
canSchedule={!!capabilities.canSchedule}
|
||||
scheduleEnabled={!!draft.schedule.enabled}
|
||||
scheduleAt={draft.schedule.at}
|
||||
onFeaturedChange={(featured) => patchDraft({ featured })}
|
||||
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
||||
/>
|
||||
|
||||
<footer className="jio-creator__footer">
|
||||
<span>{__('字数', 'jio')}:{charCount}</span>
|
||||
<span>{__('约', 'jio')} {Math.max(1, Math.round(charCount / 400))} {__('分钟阅读', 'jio')}</span>
|
||||
</footer>
|
||||
|
||||
{drawerOpen && (
|
||||
<PublishDrawer
|
||||
draft={draft}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 发布配置抽屉。
|
||||
*
|
||||
* 封面、分类、摘要、付费与资料附件都只在这里出现,正文画布保持干净;
|
||||
* 分类、标签、付费与资料附件都在这里配置;封面与定时发布在正文下方的
|
||||
* 「发布设置」卡片里,避免把封面入口和正文挤在同一张纸上。
|
||||
* 手机端以全屏面板呈现。所有字段与权限来自服务端 bootstrap,前端不自行放大权限。
|
||||
*
|
||||
* @package Jio
|
||||
@@ -12,7 +13,6 @@ import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { apiUpload } from '../services/api';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
type PayState = {
|
||||
enabled: boolean;
|
||||
modo: 'cash' | 'points';
|
||||
@@ -27,11 +27,9 @@ type PayState = {
|
||||
type Draft = {
|
||||
postId: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
featured: Featured;
|
||||
pay: PayState;
|
||||
};
|
||||
|
||||
@@ -89,7 +87,6 @@ export function PublishDrawer(props: Props) {
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const coverInput = useRef<HTMLInputElement | null>(null);
|
||||
const fileInput = useRef<HTMLInputElement | null>(null);
|
||||
const captchaSlot = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -135,28 +132,6 @@ export function PublishDrawer(props: Props) {
|
||||
setTagInput('');
|
||||
};
|
||||
|
||||
const uploadCover = async () => {
|
||||
const file = coverInput.current?.files?.[0];
|
||||
if (!file || !bootstrap.media?.uploadUrl?.image) {
|
||||
return;
|
||||
}
|
||||
setUploadError('');
|
||||
setUploading(true);
|
||||
try {
|
||||
const response = await apiUpload(bootstrap.media.uploadUrl.image, file, nonce);
|
||||
const id = Number(response.attachment_id || response.id || 0);
|
||||
const url = String(response.url || '');
|
||||
if (!id || !url) {
|
||||
throw new Error(__('上传后无法读取图片。', 'jio'));
|
||||
}
|
||||
setDraft({ featured: { type: 'image', mediaId: id, data: { id, url } } });
|
||||
} catch (error: any) {
|
||||
setUploadError(String(error?.message || __('上传失败', 'jio')));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadAttachment = async () => {
|
||||
const file = fileInput.current?.files?.[0];
|
||||
if (!file) {
|
||||
@@ -213,36 +188,6 @@ export function PublishDrawer(props: Props) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<section className="jio-creator-field">
|
||||
<h3>{__('封面', 'jio')}</h3>
|
||||
{draft.featured?.data?.url ? (
|
||||
<div className="jio-creator-cover">
|
||||
<img src={String(draft.featured.data.url)} alt="" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDraft({ featured: { type: '', data: {} } })}
|
||||
aria-label={__('移除封面', 'jio')}
|
||||
>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
) : capabilities.canUploadImage ? (
|
||||
<label className="jio-creator-cover-empty">
|
||||
<input
|
||||
ref={coverInput}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={() => void uploadCover()}
|
||||
disabled={uploading}
|
||||
/>
|
||||
<i className="fa fas fa-plus" aria-hidden="true" />
|
||||
<span>{uploading ? __('上传中…', 'jio') : __('上传封面', 'jio')}</span>
|
||||
</label>
|
||||
) : (
|
||||
<p className="jio-creator-muted">{__('当前账号不能设置封面。', 'jio')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="jio-creator-field">
|
||||
<h3>{__('分类', 'jio')}</h3>
|
||||
<div className="jio-creator-chips">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$ink: #243330;
|
||||
$ink-soft: #5c6b66;
|
||||
$paper: #fafbfa;
|
||||
$line: #e2e9e5;
|
||||
$ink: #2b2b2b;
|
||||
$ink-soft: #7d8683;
|
||||
$paper: #f6f6f6;
|
||||
$line: #e6e9e8;
|
||||
$accent: #267665;
|
||||
$accent-strong: #1d5f52;
|
||||
$warn: #b4573f;
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Jio Creator Studio — 沉浸式写作工作台。
|
||||
*
|
||||
* 视觉方向:纸面写作而不是后台配置。单一深青绿用于“保存/发布”这类推进动作,
|
||||
* 其余保持低对比;发布设置全部收进抽屉,正文区域只有标题、摘要与画布。
|
||||
* 视觉方向:浅灰页面 + 居中白纸。写作卡片居中并承担主要视觉重量,
|
||||
* 高频操作固定在顶栏,封面与定时发布收在正文下方的「发布设置」卡片里。
|
||||
*/
|
||||
|
||||
@use './tokens' as *;
|
||||
@@ -14,7 +14,7 @@
|
||||
* 只在投稿模板的 body class 下生效,不影响其他页面。
|
||||
*/
|
||||
.jio-page-submit {
|
||||
background: $paper;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
/* 主题固定导航会给 body.nav-fixed 预留 88px;工作台隐藏了导航,不应保留空位。 */
|
||||
@@ -43,7 +43,7 @@ body.jio-page-submit.has-static-nav {
|
||||
.jio-creator {
|
||||
--jc-ink: #{$ink};
|
||||
--jc-ink-soft: #{$ink-soft};
|
||||
--jc-paper: #{$paper};
|
||||
--jc-paper: #f6f6f6;
|
||||
--jc-line: #{$line};
|
||||
--jc-accent: #{$accent};
|
||||
--jc-accent-strong: #{$accent-strong};
|
||||
@@ -373,78 +373,57 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
|
||||
.jio-creator__canvas {
|
||||
/* 公众号式阅读宽度:正文行宽控制在约 40 个汉字,留白优先。 */
|
||||
width: min(720px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: clamp(20px, 4vw, 44px) clamp(20px, 5vw, 52px) 140px;
|
||||
/* 居中白纸:正文行宽固定,页面底色是浅灰,卡片本身承担视觉重量。 */
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 40px auto 0;
|
||||
padding: clamp(30px, 4vw, 50px) clamp(28px, 5vw, 56px) clamp(36px, 5vw, 56px);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(24, 39, 34, 0.04), 0 8px 28px rgba(24, 39, 34, 0.05);
|
||||
}
|
||||
|
||||
.jio-creator__title {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 0;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 0;
|
||||
min-height: 46px;
|
||||
padding: 0 0 16px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #eef0ef;
|
||||
background: transparent;
|
||||
color: var(--jc-ink);
|
||||
font-family: "Songti SC", "Noto Serif SC", "Source Han Serif SC", serif;
|
||||
font-size: clamp(23px, 2.6vw, 27px);
|
||||
font-weight: 500;
|
||||
color: #2b2b2b;
|
||||
font-family: inherit;
|
||||
font-size: clamp(26px, 3vw, 32px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.4;
|
||||
line-height: 1.35;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-bottom-color: #dfe3e1;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b7c2bd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__excerpt {
|
||||
display: block;
|
||||
margin-bottom: 22px;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #9aa8a2;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--jc-ink-soft);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
color: #c3c7c5;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__editor {
|
||||
position: relative;
|
||||
min-height: 50vh;
|
||||
margin: 12px 0 30px;
|
||||
min-height: 52vh;
|
||||
margin: 22px 0 0;
|
||||
|
||||
.wp-editor-tabs,
|
||||
.wp-media-buttons,
|
||||
.quicktags-toolbar,
|
||||
.mce-top-part {
|
||||
.mce-top-part,
|
||||
.tox .tox-editor-header,
|
||||
.tox .tox-statusbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -455,12 +434,6 @@ body.jio-page-submit.has-static-nav {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mce-top-part,
|
||||
.tox .tox-editor-header,
|
||||
.tox .tox-statusbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mce-tinymce,
|
||||
.tox-tinymce {
|
||||
border: 0 !important;
|
||||
@@ -477,76 +450,134 @@ body.jio-page-submit.has-static-nav {
|
||||
|
||||
.mce-edit-area iframe,
|
||||
.tox .tox-edit-area__iframe {
|
||||
min-height: 55vh;
|
||||
min-height: 52vh;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 55vh;
|
||||
min-height: 52vh;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--jc-ink);
|
||||
color: #2b2b2b;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
line-height: 1.85;
|
||||
line-height: 1.8;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--jc-line);
|
||||
color: var(--jc-ink-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 封面预览条 ---------- */
|
||||
|
||||
.jio-creator__cover {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&.is-empty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px dashed #d3ded8;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #8c9a94;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, color 0.16s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--jc-accent);
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
&::placeholder {
|
||||
color: #b9bfbc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-thumb {
|
||||
position: relative;
|
||||
/* ---------- 发布设置卡片 ---------- */
|
||||
|
||||
.jio-creator__settings {
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 20px auto 0;
|
||||
padding: clamp(20px, 3vw, 28px) clamp(24px, 4vw, 40px);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(24, 39, 34, 0.04), 0 8px 28px rgba(24, 39, 34, 0.05);
|
||||
}
|
||||
|
||||
.jio-creator__settings-title {
|
||||
margin: 0 0 4px;
|
||||
color: #2b2b2b;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.jio-creator__setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f2f4f3;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__setting-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jio-creator__setting-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: #2b2b2b;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.jio-creator__setting-help {
|
||||
margin: 4px 0 0;
|
||||
color: #9aa2a0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.jio-creator__cover-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jio-creator__cover-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.jio-creator__cover-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 1px dashed #d8dedb;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #7c8683;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
border-color: var(--jc-accent);
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-preview {
|
||||
display: block;
|
||||
width: 112px;
|
||||
height: 63px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #eef3f0;
|
||||
border-radius: 6px;
|
||||
background: #eef1f0;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 2.35 / 1;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@@ -556,45 +587,67 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__cover-edit {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
display: flex;
|
||||
.jio-creator__cover-clear {
|
||||
display: inline-flex;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: linear-gradient(to top, rgba(15, 25, 22, 0.62), rgba(15, 25, 22, 0));
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.jio-creator__cover-thumb:hover .jio-creator__cover-edit,
|
||||
.jio-creator__cover-thumb:focus-visible .jio-creator__cover-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.jio-creator__cover-clear {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 25, 22, 0.55);
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
color: #98a19e;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: rgba(15, 25, 22, 0.78);
|
||||
background: #f3f5f4;
|
||||
color: var(--jc-warn);
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__schedule {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0 18px;
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #6f7875;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
input[type="datetime-local"] {
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e3e7e5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #2b2b2b;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jio-creator-muted {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: min(900px, calc(100% - clamp(24px, 6vw, 72px)));
|
||||
margin: 16px auto 0;
|
||||
padding: 0 4px 140px;
|
||||
color: #98a19e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 作者行 ---------- */
|
||||
|
||||
.jio-creator__author {
|
||||
@@ -728,13 +781,30 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
|
||||
.jio-creator__canvas {
|
||||
padding: 20px 16px calc(120px + env(safe-area-inset-bottom));
|
||||
width: calc(100% - 20px);
|
||||
margin: 14px auto 0;
|
||||
padding: 22px 18px 28px;
|
||||
}
|
||||
|
||||
.jio-creator__settings {
|
||||
width: calc(100% - 20px);
|
||||
margin-top: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.jio-creator__setting-row {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__title textarea {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.jio-creator__footer {
|
||||
width: calc(100% - 20px);
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.jio-creator__editor .mce-edit-area iframe,
|
||||
.jio-creator__editor .tox .tox-edit-area__iframe {
|
||||
min-height: 48vh;
|
||||
|
||||
@@ -267,7 +267,7 @@ class SchemaPostList {
|
||||
['key' => 'post_article_custom_filter_s', 'label' => '允许选择高级筛选', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '此功能启用后,请先在文章列表/高级筛选中添加高级筛选明细'],
|
||||
['key' => 'post_article_tag', 'label' => '允许插入标签', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时候允许设置文章标签'],
|
||||
['key' => 'post_article_user', 'label' => '免登陆投稿发布用户', 'type' => 'user_select', 'default' => 1, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '当选择投稿权限为无需登录就能投稿时,投稿文章的用户'],
|
||||
['key' => 'post_article_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'between_number', 'default' => ['min' => 5, 'max' => 30], 'unit' => '字', 'description' => '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)'],
|
||||
['key' => 'post_article_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'between_number', 'default' => ['min' => 5, 'max' => 100], 'unit' => '字', 'description' => '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)'],
|
||||
['key' => 'post_article_pay_free_s', 'label' => '允许设置为免费资源', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '付费内容金额允许设置为 0'],
|
||||
['key' => 'post_article_pay_price_limit', 'label' => '付费内容金额限制', 'type' => 'fieldset', 'default' => ['price' => ['min' => 0, 'max' => 99999], 'points' => ['min' => 0, 'max' => 0]], 'description' => '限制用户允许设置的金额,防止金额过高不合理(填0为不限制)', 'fields' => [
|
||||
['key' => 'price', 'label' => '现金金额限制', 'type' => 'between_number', 'default' => ['min' => 0, 'max' => 99999]],
|
||||
@@ -276,8 +276,8 @@ class SchemaPostList {
|
||||
['key' => 'post_article_pay_vip_price_s', 'label' => '付费内容允许设置会员价', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时对拥有设置付费内容权限的用户,是否开启设置会员价格。开启此项,会直接在前台显示设置会员价的选项;关闭此项,则用户只能设置普通价格,会员价则按照下方设置的折扣自动计算'],
|
||||
['key' => 'post_article_pay_vip_1_discount', 'label' => '一级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]]],
|
||||
['key' => 'post_article_pay_vip_2_discount', 'label' => '二级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]], 'description' => '执行价的百分之多少,0为免费,100为没有折扣,不能高于100'],
|
||||
['key' => 'post_t_placeholder', 'label' => '投稿标题占位符', 'type' => 'input', 'default' => '请输入标题'],
|
||||
['key' => 'post_c_placeholder', 'label' => '投稿内容占位符', 'type' => 'input', 'default' => '请输入内容'],
|
||||
['key' => 'post_t_placeholder', 'label' => '投稿标题占位符', 'type' => 'input', 'default' => '请输入标题(最多 100 个字)'],
|
||||
['key' => 'post_c_placeholder', 'label' => '投稿内容占位符', 'type' => 'input', 'default' => '请输入正文'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -187,11 +187,11 @@ final class CreatorRest {
|
||||
$payload = [
|
||||
'title' => (string) $request->get_param('title'),
|
||||
'content' => (string) $request->get_param('content'),
|
||||
'excerpt' => (string) $request->get_param('excerpt'),
|
||||
'categories' => (array) $request->get_param('categories'),
|
||||
'tags' => (array) $request->get_param('tags'),
|
||||
'custom' => (array) $request->get_param('custom'),
|
||||
'featured' => (array) $request->get_param('featured'),
|
||||
'schedule' => (array) $request->get_param('schedule'),
|
||||
'pay' => (array) $request->get_param('pay'),
|
||||
];
|
||||
|
||||
@@ -199,7 +199,6 @@ final class CreatorRest {
|
||||
$result = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [
|
||||
'title' => $payload['title'] !== '' ? $payload['title'] : null,
|
||||
'content' => $payload['content'],
|
||||
'excerpt' => $payload['excerpt'],
|
||||
'base_rev' => absint($request->get_param('rev')),
|
||||
'snapshot' => $this->snapshot_from_payload($payload),
|
||||
], false);
|
||||
@@ -215,10 +214,10 @@ final class CreatorRest {
|
||||
'posts_id' => $post_id,
|
||||
'post_title' => $payload['title'],
|
||||
'post_content' => $payload['content'],
|
||||
'excerpt' => $payload['excerpt'],
|
||||
'category' => $payload['categories'],
|
||||
'tags' => implode(',', array_map('strval', $payload['tags'])),
|
||||
'featured_data'=> $payload['featured'],
|
||||
'schedule' => $payload['schedule'],
|
||||
];
|
||||
foreach ($payload['custom'] as $key => $value) {
|
||||
$data['cf_' . sanitize_key((string) $key)] = $value;
|
||||
@@ -259,11 +258,11 @@ final class CreatorRest {
|
||||
$payload = [
|
||||
'title' => (string) $request->get_param('title'),
|
||||
'content' => (string) $request->get_param('content'),
|
||||
'excerpt' => (string) $request->get_param('excerpt'),
|
||||
'categories' => (array) $request->get_param('categories'),
|
||||
'tags' => (array) $request->get_param('tags'),
|
||||
'custom' => (array) $request->get_param('custom'),
|
||||
'featured' => (array) $request->get_param('featured'),
|
||||
'schedule' => (array) $request->get_param('schedule'),
|
||||
'pay' => (array) $request->get_param('pay'),
|
||||
];
|
||||
|
||||
@@ -275,7 +274,6 @@ final class CreatorRest {
|
||||
$version = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [
|
||||
'title' => $payload['title'],
|
||||
'content' => $payload['content'],
|
||||
'excerpt' => $payload['excerpt'],
|
||||
'base_rev' => absint($request->get_param('rev')),
|
||||
'snapshot' => $this->snapshot_from_payload($payload),
|
||||
], true);
|
||||
@@ -299,10 +297,10 @@ final class CreatorRest {
|
||||
'posts_id' => $post_id,
|
||||
'post_title' => $payload['title'],
|
||||
'post_content' => $payload['content'],
|
||||
'excerpt' => $payload['excerpt'],
|
||||
'category' => $payload['categories'],
|
||||
'tags' => implode(',', array_map('strval', $payload['tags'])),
|
||||
'featured_data' => $payload['featured'],
|
||||
'schedule' => $payload['schedule'],
|
||||
];
|
||||
foreach ($payload['custom'] as $key => $value) {
|
||||
$data['cf_' . sanitize_key((string) $key)] = $value;
|
||||
@@ -691,6 +689,8 @@ final class CreatorRest {
|
||||
'canPay' => $cap('new_post_pay'),
|
||||
'canPayDownload' => $cap('new_post_pay_download'),
|
||||
'canPublishNow' => $cap('new_post_audit_no'),
|
||||
// 定时发布只有能直接发布的账号可用;其余账号提交后仍走审核队列。
|
||||
'canSchedule' => $cap('new_post_audit_no'),
|
||||
'canEdit' => $post_id > 0 ? $front->can_edit_post($post_id, $user_id) : $front->can_submit($user_id),
|
||||
'isLoggedIn' => $user_id > 0,
|
||||
];
|
||||
|
||||
@@ -215,6 +215,8 @@ final class CreatorStudio {
|
||||
'root' => rest_url(),
|
||||
'ajax' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
// 定时发布按站点时区解释;文案里显示给作者,避免 UTC 误解。
|
||||
'timezone' => (string) wp_timezone_string(),
|
||||
'captcha' => [
|
||||
'enabled' => function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('front_submit'),
|
||||
],
|
||||
|
||||
@@ -245,9 +245,9 @@ class FrontSubmit {
|
||||
'enabled' => true,
|
||||
'btn_text' => '发布文章',
|
||||
'title_min' => 5,
|
||||
'title_max' => 30,
|
||||
'title_placeholder' => '请输入标题',
|
||||
'content_placeholder' => '请输入内容',
|
||||
'title_max' => 100,
|
||||
'title_placeholder' => '请输入标题(最多 100 个字)',
|
||||
'content_placeholder' => '请输入正文',
|
||||
'allowed_cat_ids' => [],
|
||||
'default_user_id' => 1,
|
||||
'allow_tags' => true,
|
||||
|
||||
@@ -134,8 +134,17 @@ trait FrontSubmitSave {
|
||||
}
|
||||
|
||||
$status = 'draft';
|
||||
$schedule_at = '';
|
||||
if (!$is_draft) {
|
||||
$status = $this->resolve_publish_status($user_id);
|
||||
if ($status === 'publish') {
|
||||
// 定时发布:只有能直接发布的账号才排期,否则仍走审核队列。
|
||||
$schedule = $this->normalize_schedule($data);
|
||||
if ($schedule !== '') {
|
||||
$schedule_at = $schedule;
|
||||
$status = 'future';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paid fields are validated before anything is written, so an invalid
|
||||
@@ -161,6 +170,11 @@ trait FrontSubmitSave {
|
||||
'post_status' => $status,
|
||||
'comment_status' => 'open',
|
||||
];
|
||||
if ($schedule_at !== '') {
|
||||
// WordPress 按 post_date 到点把 future 转成 publish。
|
||||
$postarr['post_date'] = $schedule_at;
|
||||
$postarr['post_date_gmt'] = get_gmt_from_date($schedule_at);
|
||||
}
|
||||
if ($excerpt !== '' || array_key_exists('excerpt', $data) || array_key_exists('post_excerpt', $data)) {
|
||||
$postarr['post_excerpt'] = $excerpt;
|
||||
}
|
||||
@@ -223,6 +237,18 @@ trait FrontSubmitSave {
|
||||
];
|
||||
|
||||
switch ($status) {
|
||||
case 'future':
|
||||
$send['message'] = sprintf(
|
||||
/* translators: %s: scheduled publish time */
|
||||
__('文章已排期,将在 %s 发布', 'jio'),
|
||||
$schedule_at
|
||||
);
|
||||
$send['scheduled_at'] = $schedule_at;
|
||||
if ($user_id > 0 && function_exists('jio_get_user_home_url')) {
|
||||
$send['reload'] = true;
|
||||
$send['goto'] = jio_get_user_home_url($user_id);
|
||||
}
|
||||
break;
|
||||
case 'pending':
|
||||
$send['message'] = __('内容已提交,正在等待审核', 'jio');
|
||||
if ($user_id > 0 && function_exists('jio_get_user_home_url')) {
|
||||
@@ -247,6 +273,39 @@ trait FrontSubmitSave {
|
||||
return $send;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析定时发布时间(站点本地时间 Y-m-d H:i:s);未开启或不合法时返回空串。
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function normalize_schedule(array $data): string {
|
||||
$schedule = $data['schedule'] ?? null;
|
||||
if (!is_array($schedule) || empty($schedule['enabled'])) {
|
||||
return '';
|
||||
}
|
||||
$raw = trim((string) ($schedule['at'] ?? ''));
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
// datetime-local 提交 Y-m-d\TH:i(部分浏览器带秒);统一成站点本地时间。
|
||||
$normalized = str_replace('T', ' ', $raw);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $normalized) === 1) {
|
||||
$normalized .= ':00';
|
||||
}
|
||||
$timezone = wp_timezone();
|
||||
$scheduled = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $normalized, $timezone);
|
||||
$errors = \DateTimeImmutable::getLastErrors();
|
||||
if (!$scheduled instanceof \DateTimeImmutable || (is_array($errors) && (int) $errors['warning_count'] > 0)) {
|
||||
return '';
|
||||
}
|
||||
// 至少晚于当前时间 1 分钟,避免"排期到过去"被 WordPress 立即发布。
|
||||
$threshold = new \DateTimeImmutable('now', $timezone);
|
||||
if ($scheduled->getTimestamp() < $threshold->getTimestamp() + MINUTE_IN_SECONDS) {
|
||||
return '';
|
||||
}
|
||||
return $scheduled->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish status for a non-draft submission.
|
||||
*
|
||||
|
||||
@@ -351,7 +351,7 @@ class PostDisplay {
|
||||
? $settings['post_article_title_strlen_limit']
|
||||
: [];
|
||||
$title_min = max(1, min(100, (int) ($title_limit['min'] ?? 5)));
|
||||
$title_max = max(5, min(200, (int) ($title_limit['max'] ?? 30)));
|
||||
$title_max = max(5, min(200, (int) ($title_limit['max'] ?? 100)));
|
||||
if ($title_max < $title_min) {
|
||||
$title_max = $title_min;
|
||||
}
|
||||
@@ -368,8 +368,8 @@ class PostDisplay {
|
||||
'btn_text' => sanitize_text_field((string) ($settings['post_article_btn_txte'] ?? '')) ?: __('发布文章', 'jio'),
|
||||
'title_min' => $title_min,
|
||||
'title_max' => $title_max,
|
||||
'title_placeholder' => sanitize_text_field((string) ($settings['post_t_placeholder'] ?? '')) ?: __('请输入标题', 'jio'),
|
||||
'content_placeholder' => sanitize_textarea_field((string) ($settings['post_c_placeholder'] ?? '')) ?: __('请输入内容', 'jio'),
|
||||
'title_placeholder' => sanitize_text_field((string) ($settings['post_t_placeholder'] ?? '')) ?: __('请输入标题(最多 100 个字)', 'jio'),
|
||||
'content_placeholder' => sanitize_textarea_field((string) ($settings['post_c_placeholder'] ?? '')) ?: __('请输入正文', 'jio'),
|
||||
'allowed_cat_ids' => is_array($settings['post_article_cat'] ?? null) ? array_values(array_filter(array_map('intval', $settings['post_article_cat']))) : [],
|
||||
'default_user_id' => max(0, (int) ($settings['post_article_user'] ?? 1)),
|
||||
'allow_tags' => !isset($settings['post_article_tag']) || !empty($settings['post_article_tag']),
|
||||
|
||||
@@ -123,11 +123,11 @@ test('PHP submit section 默认值对齐参考主题', () => {
|
||||
assert.match(submitPhp, /'post_article_s',\s*'label'\s*=>\s*'启用前台投稿',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
|
||||
assert.match(submitPhp, /'post_article_btn_txte',\s*'label'\s*=>\s*'按钮文字',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'发布文章'/);
|
||||
assert.match(submitPhp, /'post_article_user',\s*'label'\s*=>\s*'免登陆投稿发布用户',\s*'type'\s*=>\s*'user_select',\s*'default'\s*=>\s*1/);
|
||||
assert.match(submitPhp, /'post_article_title_strlen_limit',\s*'label'\s*=>\s*'标题字数限制',\s*'type'\s*=>\s*'between_number',\s*'default'\s*=>\s*\['min'\s*=>\s*5,\s*'max'\s*=>\s*30\]/);
|
||||
assert.match(submitPhp, /'post_article_title_strlen_limit',\s*'label'\s*=>\s*'标题字数限制',\s*'type'\s*=>\s*'between_number',\s*'default'\s*=>\s*\['min'\s*=>\s*5,\s*'max'\s*=>\s*100\]/);
|
||||
assert.match(submitPhp, /'post_article_pay_vip_1_discount',\s*'label'\s*=>\s*'一级会员折扣',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*100/);
|
||||
assert.match(submitPhp, /'post_article_pay_vip_2_discount',\s*'label'\s*=>\s*'二级会员折扣',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*100/);
|
||||
assert.match(submitPhp, /'post_t_placeholder',\s*'label'\s*=>\s*'投稿标题占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入标题'/);
|
||||
assert.match(submitPhp, /'post_c_placeholder',\s*'label'\s*=>\s*'投稿内容占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入内容'/);
|
||||
assert.match(submitPhp, /'post_t_placeholder',\s*'label'\s*=>\s*'投稿标题占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入标题(最多 100 个字)'/);
|
||||
assert.match(submitPhp, /'post_c_placeholder',\s*'label'\s*=>\s*'投稿内容占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入正文'/);
|
||||
assert.match(submitPhp, /'price',\s*'label'\s*=>\s*'现金金额限制'/);
|
||||
assert.match(submitPhp, /'points',\s*'label'\s*=>\s*'积分金额限制'/);
|
||||
});
|
||||
@@ -164,8 +164,8 @@ test('JS schema 默认值对齐参考主题', () => {
|
||||
assert.match(js, /post_article_user:\s*1\b/);
|
||||
assert.match(js, /post_article_pay_vip_1_discount:\s*100\b/);
|
||||
assert.match(js, /post_article_pay_vip_2_discount:\s*100\b/);
|
||||
assert.match(js, /post_t_placeholder:\s*'请输入标题'/);
|
||||
assert.match(js, /post_c_placeholder:\s*'请输入内容'/);
|
||||
assert.match(js, /post_t_placeholder:\s*'请输入标题(最多 100 个字)'/);
|
||||
assert.match(js, /post_c_placeholder:\s*'请输入正文'/);
|
||||
assert.match(js, /price:\s*\{\s*min:\s*0,\s*max:\s*99999\s*\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -197,8 +197,44 @@ test('TinyMCE media buttons reuse Jio uploads and preserve bookmarks', () => {
|
||||
assert.match(appSource, /data-video-url/);
|
||||
assert.match(read('src/Core/CreatorStudio.php'), /front-submit\/upload\/image/);
|
||||
assert.match(read('assets/css/frontend/pages/submit.css'), /\.jio-editor-media\s*\{/);
|
||||
assert.match(read('assets/creator-studio/src/components/cover-strip.tsx'), /添加封面/);
|
||||
assert.match(appSource, /<CoverStrip/);
|
||||
assert.match(read('assets/creator-studio/src/components/publish-settings.tsx'), /添加封面/);
|
||||
assert.match(appSource, /<PublishSettings/);
|
||||
});
|
||||
|
||||
test('writing surface is a centered paper card with a publish settings card below', () => {
|
||||
const styles = read('assets/creator-studio/src/styles/studio.scss');
|
||||
const settings = read('assets/creator-studio/src/components/publish-settings.tsx');
|
||||
|
||||
assert.doesNotMatch(appSource, /jio-creator__excerpt/);
|
||||
assert.match(appSource, /请输入标题(最多 %d 个字)/, 'title placeholder must state the character limit');
|
||||
assert.match(appSource, /className="jio-creator__title"/);
|
||||
assert.match(appSource, /<PublishSettings/);
|
||||
|
||||
assert.match(styles, /\.jio-creator__canvas\s*\{[^}]*background:\s*#fff/);
|
||||
assert.match(styles, /\.jio-creator__canvas\s*\{[^}]*width:\s*min\(900px/);
|
||||
assert.match(styles, /\.jio-creator__canvas\s*\{[^}]*margin:\s*40px auto 0/);
|
||||
assert.match(styles, /\.jio-page-submit\s*\{[^}]*background:\s*#f6f6f6/);
|
||||
assert.match(styles, /\.jio-creator__settings\s*\{[^}]*background:\s*#fff/);
|
||||
assert.match(styles, /\.jio-creator__title\s*\{[\s\S]*?font-size:\s*clamp\(26px, 3vw, 32px\)/);
|
||||
assert.match(styles, /\.jio-creator__title\s*\{[\s\S]*?border-bottom:\s*1px solid/);
|
||||
|
||||
assert.match(settings, /定时发布/);
|
||||
assert.match(settings, /type="datetime-local"/);
|
||||
assert.match(settings, /canSchedule/);
|
||||
assert.match(appSource, /scheduleEnabled=\{!!draft\.schedule\.enabled\}/);
|
||||
});
|
||||
|
||||
test('scheduled publishing is validated and applied server side', () => {
|
||||
const save = read('src/Core/FrontSubmitSave.php');
|
||||
const rest = read('src/Core/CreatorRest.php');
|
||||
|
||||
assert.match(save, /private function normalize_schedule\(array \$data\): string/);
|
||||
assert.match(save, /\$status\s*=\s*'future';/);
|
||||
assert.match(save, /\$postarr\['post_date'\]\s*=\s*\$schedule_at;/);
|
||||
assert.match(save, /get_gmt_from_date\(\$schedule_at\)/);
|
||||
assert.match(save, /case 'future':/);
|
||||
assert.match(rest, /'schedule'\s*=>\s*\(array\) \$request->get_param\('schedule'\)/);
|
||||
assert.match(rest, /'canSchedule'\s*=>\s*\$cap\('new_post_audit_no'\)/);
|
||||
});
|
||||
|
||||
test('Chinese character count strips markup and counts visible text', () => {
|
||||
|
||||
Reference in new issue
Block a user