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:
kate522andClaude Opus 5 committed 2026-11-06 21:33:40 +08:00
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
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function C(e){const t=String(e||"").trim();if(!t)return"";try{const e=new URL(t,window.location.href);return"http:"===e.protocol||"https:"===e.protocol?e.href:""}catch(e){return""}}function S(e){const t=document.createElement("div");return t.innerHTML=e,(t.textContent||"").replace(/\s+/g,"")}function x(){const[r,n]=(0,t.useState)(w),i=(0,t.useRef)(r);i.current=r;const l=(0,t.useRef)(null),c=(0,t.useRef)(null),m=(0,t.useRef)(null),d=(0,t.useRef)(!1),p=(0,t.useRef)(()=>{}),[_,g]=(0,t.useState)(!1),[x,I]=(0,t.useState)({canUndo:!1,canRedo:!1}),[M,L]=(0,t.useState)("p"),[R,D]=(0,t.useState)(()=>({state:E.sourceId||E.versionId||E.content?"saved":"ready",at:""})),[P,U]=(0,t.useState)(!1),[$,F]=(0,t.useState)(null),[T,z]=(0,t.useState)(null),K=(0,t.useRef)(!1),O=(0,t.useRef)(0),A=E.capabilities||{},B={titleMin:Number(E.limits?.titleMin||1),titleMax:Number(E.limits?.titleMax||30),titlePlaceholder:String(E.limits?.titlePlaceholder||(0,a.__)("请输入标题","jio")),priceMin:Number(E.pay?.priceMin||0),priceMax:Number(E.pay?.priceMax||0),pointsMin:Number(E.pay?.pointsMin||0),pointsMax:Number(E.pay?.pointsMax||0)},J=!!E.isPublished,q=(0,t.useCallback)(e=>{K.current=!0,O.current+=1;const t={...i.current,...e};i.current=t,n(t)},[]),V=(0,t.useCallback)(e=>{if(e===i.current.content)return;K.current=!0,O.current+=1;const t={...i.current,content:e};i.current=t,n(t)},[]),H=(0,t.useCallback)(e=>{const t=e?.undoManager;t&&I({canUndo:"function"==typeof t.hasUndo&&t.hasUndo(),canRedo:"function"==typeof t.hasRedo&&t.hasRedo()})},[]),W=(0,t.useCallback)(e=>{const t=e?.selection?.getNode(),a=t?e.dom.getParent(t,"p,h1,h2,h3,blockquote")||t:null,r=a?.nodeName?.toLowerCase()||"p";L(["h1","h2","h3","blockquote"].includes(r)?r:"p")},[]),X=(0,t.useMemo)(()=>Array.from(S(r.content)).length,[r.content]),Y=(0,t.useCallback)((e={})=>{const t=i.currenLine truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(B.titleMax)||100),spellCheck:!1,maxLength:Number(B.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:N,defaultValue:r.content,onChange:e=>V(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:r.featured,canEditCover:!!A.canUploadImage,canSchedule:!!A.canSchedule,scheduleEnabled:!!r.schedule.enabled,scheduleAt:r.schedule.at,onFeaturedChange:e=>q({featured:e}),onScheduleChange:(e,t)=>q({schedule:{enabled:e,at:t}})}),(0,e.createElement)("footer",{className:"jio-creator__footer"},(0,e.createElement)("span",null,(0,a.__)("字数","jio"),":",X),(0,e.createElement)("span",null,(0,a.__)("约","jio")," ",Math.max(1,Math.round(X/400))," ",(0,a.__)("分钟阅读","jio"))),P&&(0,e.createElement)(s,{draft:r,setDraft:q,capabilities:A,limits:B,taxonomy:{categories:E.categories&&E.categories.items||[],tags:!!E.tags?.enabled,customDefs:E.customDefs||[]},isPublishedEdit:J,onClose:()=>U(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{G(e)},privateStorage:!!E.privateStorage,privateUploads:E.privateUploads||[],captcha:!!E.captcha?.enabled,saving:"saving"===R.state||"submitting"===R.state}))}function I(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(x,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()})();
+3 -1
View File
@@ -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 注释,
+1 -1
View File
@@ -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>
);
}
+34 -31
View File
@@ -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;
+199 -129
View File
@@ -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;
+3 -3
View File
@@ -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' => '请输入正文'],
]],
],
],
+6 -6
View File
@@ -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,
];
+2
View File
@@ -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'),
],
+3 -3
View File
@@ -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,
+59
View File
@@ -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.
*
+3 -3
View File
@@ -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', () => {