feat(creator-studio): two-row masthead with site logo and user avatar
顶栏改为知乎式两行结构:第一行左侧站点 LOGO、右侧当前用户头像, 第二行只保留正文工具条并居中,同时移除「Jio / 写文章」字样。 - bootstrap payload 新增 site/viewer 身份字段(复用 jio_get_site_logo_url 与 jio_get_avatar_url,前端不拼 URL、不做权限判断) - 拆分 draft 类型、TinyMCE 块运行时、顶栏组件,index.tsx 1065 → 735 行 - 清理死代码:未调用的 keepCaretInsideCode、永远无法打开的代码语言弹窗 (setCodeLanguage 只会收到 null,codeTargetRef 从未赋值)及其样式, 以及顶栏移除后失效的 bar-left/brand/mode/save-draft/ghost/primary 规则 - 契约测试、本地预览与截图脚本同步到新 DOM Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5f31c58df2
commit
1970db0b56
17 files changed
+586
-564
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Jio Creator Studio(前台沉浸式创作工作台)
|
# Jio Creator Studio(前台沉浸式创作工作台)
|
||||||
|
|
||||||
面向作者的前台写作体验:WordPress TinyMCE 负责 HTML 正文编辑,React + TypeScript 负责单行固定工具栏、写作工作台与发布抽屉,Jio 服务端继续作为权限、审核、付费与附件交付的唯一权威。
|
面向作者的前台写作体验:WordPress TinyMCE 负责 HTML 正文编辑,React + TypeScript 负责两行固定顶栏(站点身份 + 居中工具条)、写作工作台与发布抽屉,Jio 服务端继续作为权限、审核、付费与附件交付的唯一权威。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'e6c1894c680db1bc0999');
|
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '54f883fe51f636ea3a9f');
|
||||||
@@ -1 +1 @@
|
|||||||
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:64px;--jc-status:52px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-bottom:var(--jc-status);padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;width:auto}.jio-creator__tool-btn i{font-size:14px;line-height:1}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#f0f2f1;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-label{max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__format-field{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;position:relative;vertical-align:middle;width:auto}.jio-creator__format-field i{font-size:14px;line-height:1}.jio-creator__format-field:focus-within,.jio-creator__format-field:hover{background:#f0f2f1;color:var(--jc-accent-strong)}.jio-creator__format-field:focus-within{outline:2px solid rgba(38,118,101,.35);outline-offset:-2px}.jio-creator__format-select{background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;height:100%;inset:0;opacity:0;padding:0;position:absolute;width:100%}.jio-creator__format-select:disabled{cursor:default}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:vaLine truncated
|
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-top:112px;--jc-status:52px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-bottom:var(--jc-status);padding-top:var(--jc-top)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__top{background:rgba(250,251,250,.98);box-shadow:0 2px 8px rgba(27,46,39,.035);left:0;position:fixed;right:0;top:0;z-index:300}.jio-creator__masthead{align-items:center;display:flex;gap:16px;height:56px;justify-content:space-between;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__masthead-left{align-items:center;display:flex;gap:10px;min-width:0}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__brand{align-items:center;color:inherit;display:inline-flex;min-width:0;text-decoration:none}.jio-creator__brand:focus-visible{outline:2px solid rgba(38,118,101,.35);outline-offset:4px}.jio-creator__brand-logo{display:block;height:30px;max-width:168px;-o-object-fit:contain;object-fit:contain;width:auto}.jio-creator__brand-text{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__me{align-items:center;border-radius:50%;display:inline-flex;flex:0 0 auto;justify-content:center}.jio-creator__me:focus-visible{outline:2px solid rgba(38,118,101,.5);outline-offset:2px}.jio-creator__me-avatar{background:#eef1f0;border:1px solid rgba(24,39,34,.08);border-radius:50%;display:block;height:32px;-o-object-fit:cover;object-fit:cover;overflow:hidden;width:32px}.jio-creator__me-avatar.is-placeholder{align-items:center;color:#9aa2a0;display:inline-flex;font-size:13px;justify-content:center}.jio-creator__bar{align-items:center;border-bottom:1px solid var(--jc-line);border-top:1px solid var(--jc-line);display:flex;height:56px;justify-content:center;justify-content:safe center;padding:0 clamp(14px,2.4vw,30px)}.jio-creator__toolbar-tools{align-items:center;display:flex;gap:7px;max-width:100%;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;width:auto}.jio-creator__tool-btn i{font-size:14px;line-height:1}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#f0f2f1;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-label{max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__format-field{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;position:relative;vertical-align:middle;width:auto}.jio-creator__format-field i{font-size:14px;line-height:1}.jio-creator__format-field:focus-within,.jio-creator__format-field:hover{background:#f0f2f1;color:var(--jc-accent-strong)}.jio-creator__format-field:focus-within{outline:2px solid rgba(38,118,101,.35);outline-offset:-2px}.jio-creator__format-select{background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;height:100%;inset:0;opacity:0;padding:0;position:absolute;width:100%}.jio-creator__format-select:disabled{cursor:default}.jio-creator__save{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:Line truncated
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),l=await o(r);if(!r.ok)throw n(l,r.status);return l}async function l(e,t,a,r={}){const l=new FormData;l.append("file",t),Object.entries(r).forEach(([e,t])=>l.append(e,t));const i=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:l}),c=await o(i);if(!i.ok)throw n(c,i.status);return c}const i=window.jioCreatorStudio||{},c=i.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:d,taxonomy:u,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:h,captcha:j}=n,[y,E]=(0,t.useState)(""),[v,k]=(0,t.useState)(""),[N,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(o.title||"").trim().length,[o.title])>=Number(d.titleMin||1),M=()=>{const e=y.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),E("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===u.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),u.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),u.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},o.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>r({tags:o.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:y,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>E(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),M())}}),(0,e.createElement)("button",{type:"button",onClick:M},(0,a.__)("添加","jio")))),u.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=o.custom[t.key]||[],l=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(l?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:l,onChange:()=>r({custom:{...o.custom,[t.key]:l?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
|
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),i=await o(r);if(!r.ok)throw n(i,r.status);return i}async function i(e,t,a,r={}){const i=new FormData;i.append("file",t),Object.entries(r).forEach(([e,t])=>i.append(e,t));const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:i}),c=await o(l);if(!l.ok)throw n(c,l.status);return c}const l=window.jioCreatorStudio||{},c=l.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:d,taxonomy:u,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:h,captcha:j}=n,[y,E]=(0,t.useState)(""),[v,N]=(0,t.useState)(""),[k,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(o.title||"").trim().length,[o.title])>=Number(d.titleMin||1),M=()=>{const e=y.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),E("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===u.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),u.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),u.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},o.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>r({tags:o.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:y,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>E(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),M())}}),(0,e.createElement)("button",{type:"button",onClick:M},(0,a.__)("添加","jio")))),u.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=o.custom[t.key]||[],i=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(i?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:i,onChange:()=>r({custom:{...o.custom,[t.key]:i?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
|
||||||
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}function g(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 h=e=>e.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");function j(e){if(!e||-1===e.indexOf("data-jio-")&&-1===e.indexOf("jio-code-lang"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("[data-jio-math-source]").forEach(e=>{const t=e.getAttribute("data-jio-math-source")||"",a="0"!==e.getAttribute("data-jio-math-display"),n=`[jio_math display="${a?"1":"0"}"]${t}[/jio_math]`,o=e.closest("p");a&&o&&o.textContent?.trim()===e.textContent?.trim()?o.textContent=n:e.replaceWith(document.createTextNode(n))}),t.querySelectorAll("[data-jio-footnote]").forEach(e=>{const t=e.getAttribute("data-jio-footnote")||"";e.replaceWith(document.createTextNode(`[jio_footnote]${t}[/jio_footnote]`))}),t.querySelectorAll(".jio-code-lang").forEach(e=>e.remove()),t.innerHTML}function y(e){return e&&-1!==e.indexOf("[jio_")?e.replace(/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,(e,t,a)=>"0"===t?`<span class="jio-math" data-jio-math-source="${h(a)}" data-jio-math-display="0" contenteditable="false">${E(a)}</span>`:`<p><span class="jio-math jio-math--block" data-jio-math-source="${h(a)}" data-jio-math-display="1" contenteditable="false">${E(a)}</span></p>`).replace(/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,(e,t)=>`<sup class="jio-footnote-ref" data-jio-footnote="${h((e=>{const t=document.createElement("textarea");return t.innerHTML=e,t.value})(t))}" contenteditable="false">${t}</sup>`):e}const E=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/\^\{([^{}]*)\}|\^(\S)/gu,"<sup>$1$2</sup>").replace(/_\{([^{}]*)\}|_(\S)/gu,"<sub>$1$2</sub>"),v={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 k({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,v[t.state]||v.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}const N=36e5,w=24*N,C=30*w,S=365*w,x={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 M({charCount:t,saveState:n,isPublishedEdit:o,saving:r,onSaveDraft:l,onPreview:i,onPublish:c}){const s=n.at?function(e,t=Date.now()){const n=Date.parse(e);if(!Number.isFinite(n))return"";const o=Math.max(0,t-n);return o<6e4?(0,a.__)("刚刚","jio"):o<N?(0,a.sprintf)(/* translators: %d: minutes */ /* translators: %d: minutes */
|
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}function g({centerUrl:t,site:n,viewer:o}){const r=String(n.name||""),i=String(o.avatar||""),l=t||"/";return(0,e.createElement)("header",{className:"jio-creator__masthead"},(0,e.createElement)("div",{className:"jio-creator__masthead-left"},(0,e.createElement)("a",{className:"jio-creator__back",href:t||"/","aria-label":(0,a.__)("返回作者中心","jio")},(0,e.createElement)("i",{className:"fa fas fa-angle-left","aria-hidden":"true"})),(0,e.createElement)("a",{className:"jio-creator__brand",href:String(n.home||"/"),"aria-label":r||(0,a.__)("返回首页","jio")},n.logo?(0,e.createElement)("img",{className:"jio-creator__brand-logo",src:String(n.logo),alt:r}):(0,e.createElement)("span",{className:"jio-creator__brand-text"},r))),(0,e.createElement)("a",{className:"jio-creator__me",href:l,"aria-label":(0,a.__)("返回作者中心","jio")},i?(0,e.createElement)("img",{className:"jio-creator__me-avatar",src:i,alt:"",width:32,height:32}):(0,e.createElement)("span",{className:"jio-creator__me-avatar is-placeholder","aria-hidden":"true"},(0,e.createElement)("i",{className:"fa fas fa-user"}))))}function 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 j=e=>e.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");function y(e){if(!e||-1===e.indexOf("data-jio-")&&-1===e.indexOf("jio-code-lang"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("[data-jio-math-source]").forEach(e=>{const t=e.getAttribute("data-jio-math-source")||"",a="0"!==e.getAttribute("data-jio-math-display"),n=`[jio_math display="${a?"1":"0"}"]${t}[/jio_math]`,o=e.closest("p");a&&o&&o.textContent?.trim()===e.textContent?.trim()?o.textContent=n:e.replaceWith(document.createTextNode(n))}),t.querySelectorAll("[data-jio-footnote]").forEach(e=>{const t=e.getAttribute("data-jio-footnote")||"";e.replaceWith(document.createTextNode(`[jio_footnote]${t}[/jio_footnote]`))}),t.querySelectorAll(".jio-code-lang").forEach(e=>e.remove()),t.innerHTML}function E(e){return e&&-1!==e.indexOf("[jio_")?e.replace(/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,(e,t,a)=>"0"===t?`<span class="jio-math" data-jio-math-source="${j(a)}" data-jio-math-display="0" contenteditable="false">${v(a)}</span>`:`<p><span class="jio-math jio-math--block" data-jio-math-source="${j(a)}" data-jio-math-display="1" contenteditable="false">${v(a)}</span></p>`).replace(/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,(e,t)=>`<sup class="jio-footnote-ref" data-jio-footnote="${j((e=>{const t=document.createElement("textarea");return t.innerHTML=e,t.value})(t))}" contenteditable="false">${t}</sup>`):e}const v=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/\^\{([^{}]*)\}|\^(\S)/gu,"<sup>$1$2</sup>").replace(/_\{([^{}]*)\}|_(\S)/gu,"<sub>$1$2</sub>"),N={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 k({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,N[t.state]||N.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}const w=36e5,C=24*w,S=30*C,x=365*C,M={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 $({charCount:t,saveState:n,isPublishedEdit:o,saving:r,onSaveDraft:i,onPreview:l,onPublish:c}){const s=n.at?function(e,t=Date.now()){const n=Date.parse(e);if(!Number.isFinite(n))return"";const o=Math.max(0,t-n);return o<6e4?(0,a.__)("刚刚","jio"):o<w?(0,a.sprintf)(/* translators: %d: minutes */ /* translators: %d: minutes */
|
||||||
(0,a.__)("%d分钟前","jio"),Math.floor(o/6e4)):o<w?(0,a.sprintf)(/* translators: %d: hours */ /* translators: %d: hours */
|
(0,a.__)("%d分钟前","jio"),Math.floor(o/6e4)):o<C?(0,a.sprintf)(/* translators: %d: hours */ /* translators: %d: hours */
|
||||||
(0,a.__)("%d小时前","jio"),Math.floor(o/N)):o<C?(0,a.sprintf)(/* translators: %d: days */ /* translators: %d: days */
|
(0,a.__)("%d小时前","jio"),Math.floor(o/w)):o<S?(0,a.sprintf)(/* translators: %d: days */ /* translators: %d: days */
|
||||||
(0,a.__)("%d天前","jio"),Math.floor(o/w)):o<S?(0,a.sprintf)(/* translators: %d: months */ /* translators: %d: months */
|
(0,a.__)("%d天前","jio"),Math.floor(o/C)):o<x?(0,a.sprintf)(/* translators: %d: months */ /* translators: %d: months */
|
||||||
(0,a.__)("%d个月前","jio"),Math.floor(o/C)):(0,a.sprintf)(/* translators: %d: years */ /* translators: %d: years */
|
(0,a.__)("%d个月前","jio"),Math.floor(o/S)):(0,a.sprintf)(/* translators: %d: years */ /* translators: %d: years */
|
||||||
(0,a.__)("%d年前","jio"),Math.floor(o/S))}(n.at):"",d=x[n.state]||x.ready;return(0,e.createElement)("footer",{className:"jio-creator__status",role:"contentinfo"},(0,e.createElement)("div",{className:"jio-creator__status-left"},(0,e.createElement)("span",{className:"jio-creator__status-count"},(0,a.__)("字数","jio"),":",t),(0,e.createElement)("span",{className:"jio-creator__status-mode"},(0,a.__)("Markdown 语法输入中","jio"))),(0,e.createElement)("div",{className:"jio-creator__status-meta"},(0,e.createElement)(k,{value:n}),(0,e.createElement)("span",{className:"jio-creator__status-dot","aria-hidden":"true"},"·"),(0,e.createElement)("span",{className:"jio-creator__status-time"},s||d)),(0,e.createElement)("div",{className:"jio-creator__status-actions"},(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:l,disabled:r},(0,a.__)("保存草稿","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:i,disabled:r},(0,a.__)("预览","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-primary",onClick:c,disabled:r},o?(0,a.__)("提交改稿","jio"):(0,a.__)("发布","jio"))))}const $=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),q=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];const L=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),I={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function R({kind:n,onSubmit:o,onClose:r}){const[l,i]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[d,u]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,g]=(0,t.useState)(!0),[h,j]=(0,t.useState)(""),y=(0,t.useRef)(null),E=(0,t.useRef)(null),v=(0,t.useRef)(null),k=(0,t.useRef)(null);(0,t.useEffect)(()=>{(E.current||v.current||k.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const N=(0,t.useMemo)(()=>"footnote"===n?""!==h.trim():"math"!==n||""!==_.trim(),[n,h,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},I[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:E,type:"number",min:1,max:20,value:l,onChange:e=>i(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:d,onChange:e=>u(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:v,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("支持上标 ^ 与下标 _,例如 x^2 与 a_n;暂不支持完整 LaTeX。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:b,onChange:e=>g(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("独占一行显示Line truncated
|
(0,a.__)("%d年前","jio"),Math.floor(o/x))}(n.at):"",d=M[n.state]||M.ready;return(0,e.createElement)("footer",{className:"jio-creator__status",role:"contentinfo"},(0,e.createElement)("div",{className:"jio-creator__status-left"},(0,e.createElement)("span",{className:"jio-creator__status-count"},(0,a.__)("字数","jio"),":",t),(0,e.createElement)("span",{className:"jio-creator__status-mode"},(0,a.__)("Markdown 语法输入中","jio"))),(0,e.createElement)("div",{className:"jio-creator__status-meta"},(0,e.createElement)(k,{value:n}),(0,e.createElement)("span",{className:"jio-creator__status-dot","aria-hidden":"true"},"·"),(0,e.createElement)("span",{className:"jio-creator__status-time"},s||d)),(0,e.createElement)("div",{className:"jio-creator__status-actions"},(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:i,disabled:r},(0,a.__)("保存草稿","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:l,disabled:r},(0,a.__)("预览","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-primary",onClick:c,disabled:r},o?(0,a.__)("提交改稿","jio"):(0,a.__)("发布","jio"))))}const q=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),L=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];const I=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),D={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function R({kind:n,onSubmit:o,onClose:r}){const[i,l]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[d,u]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,g]=(0,t.useState)(!0),[h,j]=(0,t.useState)(""),y=(0,t.useRef)(null),E=(0,t.useRef)(null),v=(0,t.useRef)(null),N=(0,t.useRef)(null);(0,t.useEffect)(()=>{(E.current||v.current||N.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const k=(0,t.useMemo)(()=>"footnote"===n?""!==h.trim():"math"!==n||""!==_.trim(),[n,h,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},D[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:E,type:"number",min:1,max:20,value:i,onChange:e=>l(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:d,onChange:e=>u(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:v,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("支持上标 ^ 与下标 _,例如 x^2 与 a_n;暂不支持完整 LaTeX。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:b,onChange:e=>g(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("独占一行显示Line truncated
|
||||||
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(ee.titleMax)||100),spellCheck:!1,maxLength:Number(ee.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:A,defaultValue:n.content,onChange:e=>oe(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!Z.canUploadImage,canSchedule:!!Z.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>ne({featured:e}),onScheduleChange:(e,t)=>ne({schedule:{enabled:e,at:t}})}),null!==I&&(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-code-lang-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:()=>H(null)}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel"},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-code-lang-title"},(0,a.__)("代码语言","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:()=>H(null),"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},(0,e.createElement)("div",{className:"jio-creator__lang-grid"},q.map(t=>(0,e.createElement)("button",{key:t.value,type:"button",className:"jio-creator__lang-item"+(t.value===I?" is-active":""),onClick:()=>{const e=J.current;if(J.current=null,H(null),!e)return;e.setAttribute("data-enlighter-language",t.value);const a=e.querySelector("code");a&&a.setAttribute("data-enlighter-language",t.value),B(i.current),oe(j(i.current.getContent()))}},t.label)))))),x&&(0,e.createElement)(R,{kind:x,onSubmit:ve,onClose:()=>L(null)}),(0,e.createElement)(M,{charCount:ae,saveState:N,isPublishedEdit:te,saving:!_||"saving"===N.state||"submitting"===N.state,onSaveDraft:()=>{ce(!1)},onPreview:()=>{de()},onPublish:()=>S(!0)}),C&&(0,e.createElement)(s,{draft:n,setDraft:ne,capabilities:Z,limits:ee,taxonomy:{categories:D.categories&&D.categories.items||[],tags:!!D.tags?.enabled,customDefs:D.customDefs||[]},isPublishedEdit:te,onClose:()=>S(!1),onSaveDraft:()=>{ce(!1)},onSubmit:e=>{se(e)},privateStorage:!!D.privateStorage,privateUploads:D.privateUploads||[],captcha:!!D.captcha?.enabled,saving:"saving"===N.state||"submitting"===N.state}))}function J(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(H,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",J):J()})();
|
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(Y.titleMax)||100),spellCheck:!1,maxLength:Number(Y.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:F,defaultValue:n.content,onChange:e=>te(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!X.canUploadImage,canSchedule:!!X.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>ee({featured:e}),onScheduleChange:(e,t)=>ee({schedule:{enabled:e,at:t}})}),M&&(0,e.createElement)(R,{kind:M,onSubmit:je,onClose:()=>L(null)}),(0,e.createElement)($,{charCount:Z,saveState:w,isPublishedEdit:G,saving:!_||"saving"===w.state||"submitting"===w.state,onSaveDraft:()=>{re(!1)},onPreview:()=>{le()},onPublish:()=>x(!0)}),S&&(0,e.createElement)(s,{draft:n,setDraft:ee,capabilities:X,limits:Y,taxonomy:{categories:U.categories&&U.categories.items||[],tags:!!U.tags?.enabled,customDefs:U.customDefs||[]},isPublishedEdit:G,onClose:()=>x(!1),onSaveDraft:()=>{re(!1)},onSubmit:e=>{ie(e)},privateStorage:!!U.privateStorage,privateUploads:U.privateUploads||[],captcha:!!U.captcha?.enabled,saving:"saving"===w.state||"submitting"===w.state}))}function V(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(J,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",V):V()})();
|
||||||
@@ -31,7 +31,8 @@ const URL = process.argv[2] || 'https://localhost/jio-preview.html';
|
|||||||
await page.waitForFunction(() => window.tinymce?.get('jio-creator-content'), { timeout: 20000 });
|
await page.waitForFunction(() => window.tinymce?.get('jio-creator-content'), { timeout: 20000 });
|
||||||
|
|
||||||
const before = await page.evaluate(() => ({
|
const before = await page.evaluate(() => ({
|
||||||
fixed: getComputedStyle(document.querySelector('.jio-creator__bar')).position === 'fixed',
|
// 两行顶栏由外层容器整体固定,工具条本身不再单独 fixed。
|
||||||
|
fixed: getComputedStyle(document.querySelector('.jio-creator__top')).position === 'fixed',
|
||||||
toolbar: !!document.querySelector('[role="toolbar"]'),
|
toolbar: !!document.querySelector('[role="toolbar"]'),
|
||||||
editor: !!window.tinymce?.get('jio-creator-content'),
|
editor: !!window.tinymce?.get('jio-creator-content'),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -73,6 +73,30 @@ const mockBootstrap = {
|
|||||||
media: { maxBytes: 5242880, batch: 6 },
|
media: { maxBytes: 5242880, batch: 6 },
|
||||||
rest: { root: 'https://localhost/wp-json/', nonce: 'preview' },
|
rest: { root: 'https://localhost/wp-json/', nonce: 'preview' },
|
||||||
urls: { submit: '#', newPost: '#', center: '#' },
|
urls: { submit: '#', newPost: '#', center: '#' },
|
||||||
|
// 预览用内联 SVG,避免本地截图时依赖真实站点资源。
|
||||||
|
site: {
|
||||||
|
home: '#',
|
||||||
|
name: 'Jio',
|
||||||
|
logo:
|
||||||
|
'data:image/svg+xml;charset=utf-8,' +
|
||||||
|
encodeURIComponent(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="132" height="30" viewBox="0 0 132 30">' +
|
||||||
|
'<text x="0" y="23" font-family="Songti SC, Noto Serif SC, serif" font-size="24" font-weight="700" fill="#1d5f52">Jio</text>' +
|
||||||
|
'<text x="42" y="23" font-family="system-ui, sans-serif" font-size="13" fill="#7d8683">写作中心</text>' +
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
viewer: {
|
||||||
|
avatar:
|
||||||
|
'data:image/svg+xml;charset=utf-8,' +
|
||||||
|
encodeURIComponent(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">' +
|
||||||
|
'<rect width="72" height="72" fill="#dbe7e2"/>' +
|
||||||
|
'<circle cx="36" cy="28" r="12" fill="#8fb3a8"/>' +
|
||||||
|
'<path d="M12 72c0-13.3 10.7-24 24-24s24 10.7 24 24z" fill="#8fb3a8"/>' +
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
},
|
||||||
captcha: { enabled: false },
|
captcha: { enabled: false },
|
||||||
creator: {
|
creator: {
|
||||||
saveUrl: '#',
|
saveUrl: '#',
|
||||||
|
|||||||
@@ -55,14 +55,16 @@ const PREFIX = process.argv[3] || 'C:/Users/Xing/Desktop/WP-Date/data/wordpress/
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
masthead: pick('.jio-creator__masthead'),
|
||||||
|
brandLogo: pick('.jio-creator__brand-logo'),
|
||||||
|
avatar: pick('.jio-creator__me-avatar'),
|
||||||
bar: pick('.jio-creator__bar'),
|
bar: pick('.jio-creator__bar'),
|
||||||
title: pick('.jio-creator__title textarea'),
|
title: pick('.jio-creator__title textarea'),
|
||||||
settings: pick('.jio-creator__settings'),
|
settings: pick('.jio-creator__settings'),
|
||||||
canvas: pick('.jio-creator__canvas'),
|
canvas: pick('.jio-creator__canvas'),
|
||||||
toolbar: pick('.jio-creator__toolbar-tools'),
|
toolbar: pick('.jio-creator__toolbar-tools'),
|
||||||
editor: pick('.jio-creator__editor .mce-edit-area'),
|
editor: pick('.jio-creator__editor .mce-edit-area'),
|
||||||
primaryBtn: pick('.jio-creator__primary'),
|
publishBtn: pick('.jio-creator__status-primary'),
|
||||||
ghostBtn: pick('.jio-creator__ghost'),
|
|
||||||
pageBg: getComputedStyle(document.body).backgroundColor,
|
pageBg: getComputedStyle(document.body).backgroundColor,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,12 +74,14 @@ if (!EDGE) {
|
|||||||
await new Promise((r) => setTimeout(r, 3000));
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
|
||||||
const state = await page.evaluate(() => ({
|
const state = await page.evaluate(() => ({
|
||||||
|
hasMasthead: !!document.querySelector('.jio-creator__masthead'),
|
||||||
|
hasBrandLogo: !!document.querySelector('.jio-creator__brand-logo'),
|
||||||
|
hasAvatar: !!document.querySelector('.jio-creator__me-avatar'),
|
||||||
hasBar: !!document.querySelector('.jio-creator__bar'),
|
hasBar: !!document.querySelector('.jio-creator__bar'),
|
||||||
hasToolbar: !!document.querySelector('.jio-creator__toolbar-tools'),
|
hasToolbar: !!document.querySelector('.jio-creator__toolbar-tools'),
|
||||||
hasTinyMce: !!window.tinymce?.get('jio-creator-content'),
|
hasTinyMce: !!window.tinymce?.get('jio-creator-content'),
|
||||||
hasEditorFrame: !!document.querySelector('.mce-tinymce iframe, .tox-tinymce iframe'),
|
hasEditorFrame: !!document.querySelector('.mce-tinymce iframe, .tox-tinymce iframe'),
|
||||||
hasTitle: !!document.querySelector('.jio-creator__title textarea'),
|
hasTitle: !!document.querySelector('.jio-creator__title textarea'),
|
||||||
hasSaveDraft: !!document.querySelector('.jio-creator__save-draft'),
|
|
||||||
toolbarActions: Array.from(document.querySelectorAll('.jio-creator__toolbar-tools button')).map((button) => button.getAttribute('aria-label') || button.textContent.trim()),
|
toolbarActions: Array.from(document.querySelectorAll('.jio-creator__toolbar-tools button')).map((button) => button.getAttribute('aria-label') || button.textContent.trim()),
|
||||||
saveText: (document.querySelector('.jio-creator__save') || {}).textContent,
|
saveText: (document.querySelector('.jio-creator__save') || {}).textContent,
|
||||||
rejections: window.__jioRejections || [],
|
rejections: window.__jioRejections || [],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 固定顶栏里的正文工具条。
|
* 顶栏第二行里的正文工具条。
|
||||||
*
|
*
|
||||||
* 每个按钮都是「上图标 + 下文字」,与公众号/知乎的单行工具栏一致;
|
* 每个按钮都是「上图标 + 下文字」,与公众号/知乎的单行工具栏一致;
|
||||||
* 只渲染当前账号真正有权限使用的能力,全部动作由父组件转给 TinyMCE。
|
* 只渲染当前账号真正有权限使用的能力,全部动作由父组件转给 TinyMCE。
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 创作页顶栏(第一行):站点 LOGO 在左,用户头像在右。
|
||||||
|
*
|
||||||
|
* 与知乎写作页一致:品牌与身份常驻顶部,正文工具条单独占第二行并居中,
|
||||||
|
* 避免品牌文字挤占工具条的可视宽度。
|
||||||
|
*
|
||||||
|
* @package Jio
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
centerUrl: string;
|
||||||
|
site: { home?: string; name?: string; logo?: string };
|
||||||
|
viewer: { avatar?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StudioMasthead({ centerUrl, site, viewer }: Props) {
|
||||||
|
const brandName = String(site.name || '');
|
||||||
|
const avatarUrl = String(viewer.avatar || '');
|
||||||
|
const avatarHref = centerUrl || '/';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="jio-creator__masthead">
|
||||||
|
<div className="jio-creator__masthead-left">
|
||||||
|
<a className="jio-creator__back" href={centerUrl || '/'} aria-label={__('返回作者中心', 'jio')}>
|
||||||
|
<i className="fa fas fa-angle-left" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="jio-creator__brand"
|
||||||
|
href={String(site.home || '/')}
|
||||||
|
aria-label={brandName || __('返回首页', 'jio')}
|
||||||
|
>
|
||||||
|
{site.logo ? (
|
||||||
|
<img className="jio-creator__brand-logo" src={String(site.logo)} alt={brandName} />
|
||||||
|
) : (
|
||||||
|
<span className="jio-creator__brand-text">{brandName}</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a className="jio-creator__me" href={avatarHref} aria-label={__('返回作者中心', 'jio')}>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img className="jio-creator__me-avatar" src={avatarUrl} alt="" width={32} height={32} />
|
||||||
|
) : (
|
||||||
|
<span className="jio-creator__me-avatar is-placeholder" aria-hidden="true">
|
||||||
|
<i className="fa fas fa-user" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 创作工作台的草稿模型。
|
||||||
|
*
|
||||||
|
* 只描述前端持有的可编辑状态;权限、审核与付费规则始终以服务端 bootstrap 为准。
|
||||||
|
*
|
||||||
|
* @package Jio
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||||
|
|
||||||
|
export type PayState = {
|
||||||
|
enabled: boolean;
|
||||||
|
modo: 'cash' | 'points';
|
||||||
|
price: string;
|
||||||
|
points: number;
|
||||||
|
vip1Price: string;
|
||||||
|
vip2Price: string;
|
||||||
|
payLimit: number;
|
||||||
|
downloads: Array<Record<string, any>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Limits = {
|
||||||
|
titleMin?: number;
|
||||||
|
titleMax?: number;
|
||||||
|
titlePlaceholder?: string;
|
||||||
|
priceMin?: number;
|
||||||
|
priceMax?: number;
|
||||||
|
pointsMin?: number;
|
||||||
|
pointsMax?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Schedule = { enabled: boolean; at: string };
|
||||||
|
|
||||||
|
export type Draft = {
|
||||||
|
postId: number;
|
||||||
|
versionId: number;
|
||||||
|
rev: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
schedule: Schedule;
|
||||||
|
categories: number[];
|
||||||
|
tags: string[];
|
||||||
|
custom: Record<string, string[]>;
|
||||||
|
featured: Featured;
|
||||||
|
pay: PayState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initialDraft(bootstrap: Record<string, any>): Draft {
|
||||||
|
const pay = bootstrap.pay || {};
|
||||||
|
const selected = (bootstrap.categories && bootstrap.categories.selected) || [];
|
||||||
|
const tags = (bootstrap.tags && bootstrap.tags.items) || [];
|
||||||
|
return {
|
||||||
|
postId: Number(bootstrap.sourceId || 0),
|
||||||
|
versionId: Number(bootstrap.versionId || 0),
|
||||||
|
rev: Number(bootstrap.versionRev || 0),
|
||||||
|
title: String(bootstrap.title || ''),
|
||||||
|
content: String(bootstrap.content || ''),
|
||||||
|
schedule: { enabled: false, at: '' },
|
||||||
|
categories: selected.map(Number),
|
||||||
|
tags: tags.slice(),
|
||||||
|
custom: bootstrap.custom || {},
|
||||||
|
featured: bootstrap.featured || { type: '', data: {} },
|
||||||
|
pay: {
|
||||||
|
enabled: !!pay.enabled,
|
||||||
|
modo: pay.payModo === 'points' ? 'points' : 'cash',
|
||||||
|
price: String(pay.price || '0.00'),
|
||||||
|
points: Number(pay.points || 0),
|
||||||
|
vip1Price: String(pay.vip1Price || '0.00'),
|
||||||
|
vip2Price: String(pay.vip2Price || '0.00'),
|
||||||
|
payLimit: Number(pay.payLimit || 0),
|
||||||
|
downloads: Array.isArray(pay.downloads) ? pay.downloads : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* TinyMCE 编辑器增强:块级删除、代码块语言角标、自适应高度。
|
||||||
|
*
|
||||||
|
* 这些行为都作用在编辑器 iframe 内部,与 React 状态无关,因此独立成模块。
|
||||||
|
*
|
||||||
|
* @package Jio
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CODE_LANGUAGES } from './utils/block-markup';
|
||||||
|
|
||||||
|
/** 可整体删除的块级元素选择器。 */
|
||||||
|
const BLOCK_SELECTOR = 'table,pre.enlighter-pre,hr,.quote_q,p';
|
||||||
|
|
||||||
|
/** 可整体删除的块级元素(排除普通段落)。 */
|
||||||
|
const STRUCTURAL_SELECTOR = 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backspace / Delete 删除相邻的块级元素。
|
||||||
|
*
|
||||||
|
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
|
||||||
|
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
|
||||||
|
* 且紧邻目标块时直接移除该块。
|
||||||
|
*/
|
||||||
|
export function removeAdjacentBlock(editor: any, event: any): void {
|
||||||
|
const key = Number(event.keyCode);
|
||||||
|
if (key !== 8 && key !== 46) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection = editor.selection;
|
||||||
|
if (!selection || !selection.isCollapsed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = editor.getBody();
|
||||||
|
const block = selection.getNode();
|
||||||
|
const isBlank = (node: any): boolean =>
|
||||||
|
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
|
||||||
|
!editor.dom.getParent(node, STRUCTURAL_SELECTOR, body);
|
||||||
|
const isBlockElement = (node: any): boolean =>
|
||||||
|
!!node && node.nodeType === 1 && !!(node.matches?.(STRUCTURAL_SELECTOR));
|
||||||
|
|
||||||
|
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
|
||||||
|
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
|
||||||
|
const blankContainer = isBlank(block) ? block : null;
|
||||||
|
const direct = blankContainer
|
||||||
|
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (isBlockElement(direct)) {
|
||||||
|
event.preventDefault();
|
||||||
|
editor.dom.remove(direct);
|
||||||
|
editor.nodeChanged();
|
||||||
|
editor.undoManager.add();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 光标停在块首/块尾时,删除块本身。
|
||||||
|
const caretBlock = editor.dom.getParent(block, STRUCTURAL_SELECTOR, body);
|
||||||
|
if (!caretBlock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const caretIsAtEdge = (start: boolean): boolean => {
|
||||||
|
const probe = editor.dom.createRng();
|
||||||
|
probe.selectNodeContents(caretBlock);
|
||||||
|
probe.collapse(start);
|
||||||
|
const caret = selection.getRng();
|
||||||
|
return caret && typeof caret.compareBoundaryPoints === 'function'
|
||||||
|
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const parent = caretBlock.parentElement;
|
||||||
|
editor.dom.remove(caretBlock);
|
||||||
|
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
|
||||||
|
if (parent && parent.children.length === 0) {
|
||||||
|
editor.setContent('<p></p>');
|
||||||
|
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||||
|
}
|
||||||
|
editor.nodeChanged();
|
||||||
|
editor.undoManager.add();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
|
||||||
|
*
|
||||||
|
* TinyMCE 的 iframe 不会自动撑高;这里测量内容高度并同步容器,
|
||||||
|
* 页面整体滚动由外层工作台负责。
|
||||||
|
*/
|
||||||
|
export function syncEditorHeight(editor: any): void {
|
||||||
|
if (!editor || editor.removed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const doc = editor.getDoc();
|
||||||
|
const iframe = editor.getContentAreaContainer()?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||||
|
if (!doc || !iframe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const height = Math.max(
|
||||||
|
320,
|
||||||
|
doc.documentElement.scrollHeight,
|
||||||
|
doc.body ? doc.body.scrollHeight : 0
|
||||||
|
);
|
||||||
|
iframe.style.height = `${height}px`;
|
||||||
|
iframe.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在编辑器里为每个代码块渲染语言角标。
|
||||||
|
*
|
||||||
|
* 角标是编辑期提示(纯 UI),保存前由 serializeBlocks 移除。
|
||||||
|
*/
|
||||||
|
export function syncCodeLabels(editor: any): void {
|
||||||
|
const root: HTMLElement | null = editor?.getBody?.() ?? null;
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.querySelectorAll<HTMLElement>('pre.enlighter-pre[data-jio-code]').forEach((pre) => {
|
||||||
|
const existing = pre.querySelector(':scope > .jio-code-lang');
|
||||||
|
const language = pre.getAttribute('data-enlighter-language') || 'generic';
|
||||||
|
const label = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
||||||
|
if (existing) {
|
||||||
|
existing.setAttribute('data-jio-code-lang', language);
|
||||||
|
if ((existing.textContent || '') !== label) {
|
||||||
|
existing.textContent = label;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'jio-code-lang';
|
||||||
|
badge.contentEditable = 'false';
|
||||||
|
badge.setAttribute('data-jio-code-lang', language);
|
||||||
|
badge.textContent = label;
|
||||||
|
pre.insertBefore(badge, pre.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
||||||
|
*
|
||||||
|
* 与 WPS 的块操作一致:鼠标悬停到块上时在右上角出现删除入口,
|
||||||
|
* 避免只能靠反复按退格键。
|
||||||
|
*/
|
||||||
|
export function installBlockActions(editor: any): void {
|
||||||
|
const doc = editor.getDoc();
|
||||||
|
if (!doc || doc.getElementById('jio-block-actions')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bar = doc.createElement('div');
|
||||||
|
bar.id = 'jio-block-actions';
|
||||||
|
bar.className = 'jio-creator__block-actions';
|
||||||
|
bar.innerHTML =
|
||||||
|
'<button type="button" class="jio-creator__block-action" data-jio-block-delete title="' +
|
||||||
|
'删除此块' +
|
||||||
|
'" aria-label="删除此块"><span aria-hidden="true">✕</span></button>';
|
||||||
|
doc.body.appendChild(bar);
|
||||||
|
|
||||||
|
let current: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
current = null;
|
||||||
|
bar.classList.remove('is-visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
const show = (block: HTMLElement) => {
|
||||||
|
if (!block || block === doc.body) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 空段落没有删除价值,交给退格键处理。
|
||||||
|
if (block.tagName === 'P' && (block.textContent || '').trim() === '') {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current = block;
|
||||||
|
const rect = block.getBoundingClientRect();
|
||||||
|
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
|
||||||
|
bar.style.left = `${rect.right - 40}px`;
|
||||||
|
bar.classList.add('is-visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
doc.addEventListener('mouseover', (event: MouseEvent) => {
|
||||||
|
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
|
||||||
|
if (block && block !== doc.body && !block.classList.contains('jio-code-lang')) {
|
||||||
|
show(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.addEventListener('mouseleave', hide);
|
||||||
|
|
||||||
|
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
|
||||||
|
bar.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
const button = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
|
||||||
|
if (!button || !current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const target = current;
|
||||||
|
hide();
|
||||||
|
editor.undoManager.transact(() => {
|
||||||
|
editor.dom.remove(target);
|
||||||
|
});
|
||||||
|
editor.nodeChanged();
|
||||||
|
editor.undoManager.add();
|
||||||
|
if (editor.getBody().children.length === 0) {
|
||||||
|
editor.setContent('<p></p>');
|
||||||
|
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||||
|
}
|
||||||
|
syncEditorHeight(editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { apiPost } from './services/api';
|
|||||||
import { PublishDrawer } from './panels/publish-drawer';
|
import { PublishDrawer } from './panels/publish-drawer';
|
||||||
import { EditorToolbar } from './components/editor-toolbar';
|
import { EditorToolbar } from './components/editor-toolbar';
|
||||||
import { PublishSettings } from './components/publish-settings';
|
import { PublishSettings } from './components/publish-settings';
|
||||||
|
import { StudioMasthead } from './components/studio-masthead';
|
||||||
import { type SaveStateValue } from './components/save-state';
|
import { type SaveStateValue } from './components/save-state';
|
||||||
import { useAutosave } from './hooks/use-autosave';
|
import { useAutosave } from './hooks/use-autosave';
|
||||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||||
@@ -22,7 +23,6 @@ import { EDITOR_CONTENT_STYLE } from './editor-content-style';
|
|||||||
import { hydrateBlocks, serializeBlocks } from './utils/serialize';
|
import { hydrateBlocks, serializeBlocks } from './utils/serialize';
|
||||||
import { StatusBar } from './components/status-bar';
|
import { StatusBar } from './components/status-bar';
|
||||||
import {
|
import {
|
||||||
CODE_LANGUAGES,
|
|
||||||
codeBlockMarkup,
|
codeBlockMarkup,
|
||||||
dividerMarkup,
|
dividerMarkup,
|
||||||
footnoteMarkup,
|
footnoteMarkup,
|
||||||
@@ -34,6 +34,13 @@ import {
|
|||||||
type QuoteColor,
|
type QuoteColor,
|
||||||
} from './utils/block-markup';
|
} from './utils/block-markup';
|
||||||
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
||||||
|
import { type Draft, type Limits, initialDraft } from './draft';
|
||||||
|
import {
|
||||||
|
installBlockActions,
|
||||||
|
removeAdjacentBlock,
|
||||||
|
syncCodeLabels,
|
||||||
|
syncEditorHeight,
|
||||||
|
} from './editor-runtime';
|
||||||
import './styles/studio.scss';
|
import './styles/studio.scss';
|
||||||
|
|
||||||
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
||||||
@@ -41,296 +48,6 @@ const nonce: string = bootstrap.nonce || '';
|
|||||||
const STORAGE_KEY = `jio-studio-${bootstrap.sourceId || 0}`;
|
const STORAGE_KEY = `jio-studio-${bootstrap.sourceId || 0}`;
|
||||||
const EDITOR_ID = 'jio-creator-content';
|
const EDITOR_ID = 'jio-creator-content';
|
||||||
|
|
||||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
|
||||||
type PayState = {
|
|
||||||
enabled: boolean;
|
|
||||||
modo: 'cash' | 'points';
|
|
||||||
price: string;
|
|
||||||
points: number;
|
|
||||||
vip1Price: string;
|
|
||||||
vip2Price: string;
|
|
||||||
payLimit: number;
|
|
||||||
downloads: Array<Record<string, any>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Limits = {
|
|
||||||
titleMin?: number;
|
|
||||||
titleMax?: number;
|
|
||||||
titlePlaceholder?: string;
|
|
||||||
priceMin?: number;
|
|
||||||
priceMax?: number;
|
|
||||||
pointsMin?: number;
|
|
||||||
pointsMax?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Schedule = { enabled: boolean; at: string };
|
|
||||||
|
|
||||||
type Draft = {
|
|
||||||
postId: number;
|
|
||||||
versionId: number;
|
|
||||||
rev: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
schedule: Schedule;
|
|
||||||
categories: number[];
|
|
||||||
tags: string[];
|
|
||||||
custom: Record<string, string[]>;
|
|
||||||
featured: Featured;
|
|
||||||
pay: PayState;
|
|
||||||
};
|
|
||||||
|
|
||||||
function initialDraft(): Draft {
|
|
||||||
const pay = bootstrap.pay || {};
|
|
||||||
const selected = (bootstrap.categories && bootstrap.categories.selected) || [];
|
|
||||||
const tags = (bootstrap.tags && bootstrap.tags.items) || [];
|
|
||||||
return {
|
|
||||||
postId: Number(bootstrap.sourceId || 0),
|
|
||||||
versionId: Number(bootstrap.versionId || 0),
|
|
||||||
rev: Number(bootstrap.versionRev || 0),
|
|
||||||
title: String(bootstrap.title || ''),
|
|
||||||
content: String(bootstrap.content || ''),
|
|
||||||
schedule: { enabled: false, at: '' },
|
|
||||||
categories: selected.map(Number),
|
|
||||||
tags: tags.slice(),
|
|
||||||
custom: bootstrap.custom || {},
|
|
||||||
featured: bootstrap.featured || { type: '', data: {} },
|
|
||||||
pay: {
|
|
||||||
enabled: !!pay.enabled,
|
|
||||||
modo: pay.payModo === 'points' ? 'points' : 'cash',
|
|
||||||
price: String(pay.price || '0.00'),
|
|
||||||
points: Number(pay.points || 0),
|
|
||||||
vip1Price: String(pay.vip1Price || '0.00'),
|
|
||||||
vip2Price: String(pay.vip2Price || '0.00'),
|
|
||||||
payLimit: Number(pay.payLimit || 0),
|
|
||||||
downloads: Array.isArray(pay.downloads) ? pay.downloads : [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backspace / Delete 删除相邻的块级元素。
|
|
||||||
*
|
|
||||||
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
|
|
||||||
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
|
|
||||||
* 且紧邻目标块时直接移除该块。
|
|
||||||
*/
|
|
||||||
function removeAdjacentBlock(editor: any, event: any): void {
|
|
||||||
const key = Number(event.keyCode);
|
|
||||||
if (key !== 8 && key !== 46) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selection = editor.selection;
|
|
||||||
if (!selection || !selection.isCollapsed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = editor.getBody();
|
|
||||||
const block = selection.getNode();
|
|
||||||
const isBlank = (node: any): boolean =>
|
|
||||||
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
|
|
||||||
!editor.dom.getParent(node, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
|
||||||
const isBlockElement = (node: any): boolean =>
|
|
||||||
!!node && node.nodeType === 1 && !!(node.matches?.('table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder'));
|
|
||||||
|
|
||||||
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
|
|
||||||
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
|
|
||||||
const blankContainer = isBlank(block) ? block : null;
|
|
||||||
const direct = blankContainer
|
|
||||||
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (isBlockElement(direct)) {
|
|
||||||
event.preventDefault();
|
|
||||||
editor.dom.remove(direct);
|
|
||||||
editor.nodeChanged();
|
|
||||||
editor.undoManager.add();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 光标停在块首/块尾时,删除块本身。
|
|
||||||
const caretBlock = editor.dom.getParent(block, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
|
||||||
if (!caretBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const caretIsAtEdge = (start: boolean): boolean => {
|
|
||||||
const probe = editor.dom.createRng();
|
|
||||||
probe.selectNodeContents(caretBlock);
|
|
||||||
probe.collapse(start);
|
|
||||||
const caret = selection.getRng();
|
|
||||||
return caret && typeof caret.compareBoundaryPoints === 'function'
|
|
||||||
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const parent = caretBlock.parentElement;
|
|
||||||
editor.dom.remove(caretBlock);
|
|
||||||
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
|
|
||||||
if (parent && parent.children.length === 0) {
|
|
||||||
editor.setContent('<p></p>');
|
|
||||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
|
||||||
}
|
|
||||||
editor.nodeChanged();
|
|
||||||
editor.undoManager.add();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在编辑器里为每个代码块渲染语言角标,并保证回车不会跳出代码区。
|
|
||||||
*
|
|
||||||
* 角标是编辑期提示(纯 UI),保存前由 serializeBlocks 移除。
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* 在代码块内回车时保持在代码里换行,而不是跳出到普通段落。
|
|
||||||
*
|
|
||||||
* TinyMCE 默认把 pre 当成不可持续输入的块,回车会新建段落,导致代码跑到
|
|
||||||
* 代码块外面。这里手动插入换行并保持光标在 code 内。
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
|
|
||||||
*
|
|
||||||
* TinyMCE 的 iframe 不会自动撑高;这里测量内容高度并同步容器,
|
|
||||||
* 页面整体滚动由外层工作台负责。
|
|
||||||
*/
|
|
||||||
function syncEditorHeight(editor: any): void {
|
|
||||||
if (!editor || editor.removed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const doc = editor.getDoc();
|
|
||||||
const iframe = editor.getContentAreaContainer()?.querySelector('iframe') as HTMLIFrameElement | null;
|
|
||||||
if (!doc || !iframe) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const height = Math.max(
|
|
||||||
320,
|
|
||||||
doc.documentElement.scrollHeight,
|
|
||||||
doc.body ? doc.body.scrollHeight : 0
|
|
||||||
);
|
|
||||||
iframe.style.height = `${height}px`;
|
|
||||||
iframe.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function keepCaretInsideCode(editor: any, event: any): boolean {
|
|
||||||
if (Number(event.keyCode) !== 13 || event.shiftKey) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pre = editor.dom.getParent(editor.selection.getNode(), 'pre.enlighter-pre');
|
|
||||||
if (!pre) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
const code = pre.querySelector('code') || pre;
|
|
||||||
editor.selection.setCursorLocation(code, code.childNodes.length);
|
|
||||||
editor.insertContent('<br>');
|
|
||||||
editor.nodeChanged();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 可整体删除的块级元素选择器。 */
|
|
||||||
const BLOCK_SELECTOR = 'table,pre.enlighter-pre,hr,.quote_q,p';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
|
||||||
*
|
|
||||||
* 与 WPS 的块操作一致:鼠标悬停到块上时在右上角出现删除入口,
|
|
||||||
* 避免只能靠反复按退格键。
|
|
||||||
*/
|
|
||||||
function installBlockActions(editor: any): void {
|
|
||||||
const doc = editor.getDoc();
|
|
||||||
if (!doc || doc.getElementById('jio-block-actions')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bar = doc.createElement('div');
|
|
||||||
bar.id = 'jio-block-actions';
|
|
||||||
bar.className = 'jio-creator__block-actions';
|
|
||||||
bar.innerHTML =
|
|
||||||
'<button type="button" class="jio-creator__block-action" data-jio-block-delete title="' +
|
|
||||||
'删除此块' +
|
|
||||||
'" aria-label="删除此块"><span aria-hidden="true">✕</span></button>';
|
|
||||||
doc.body.appendChild(bar);
|
|
||||||
|
|
||||||
let current: HTMLElement | null = null;
|
|
||||||
|
|
||||||
const hide = () => {
|
|
||||||
current = null;
|
|
||||||
bar.classList.remove('is-visible');
|
|
||||||
};
|
|
||||||
|
|
||||||
const show = (block: HTMLElement) => {
|
|
||||||
if (!block || block === doc.body) {
|
|
||||||
hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 空段落没有删除价值,交给退格键处理。
|
|
||||||
if (block.tagName === 'P' && (block.textContent || '').trim() === '') {
|
|
||||||
hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
current = block;
|
|
||||||
const rect = block.getBoundingClientRect();
|
|
||||||
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
|
|
||||||
bar.style.left = `${rect.right - 40}px`;
|
|
||||||
bar.classList.add('is-visible');
|
|
||||||
};
|
|
||||||
|
|
||||||
doc.addEventListener('mouseover', (event: MouseEvent) => {
|
|
||||||
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
|
|
||||||
if (block && block !== doc.body && !block.classList.contains('jio-code-lang')) {
|
|
||||||
show(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.addEventListener('mouseleave', hide);
|
|
||||||
|
|
||||||
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
|
|
||||||
bar.addEventListener('click', (event: MouseEvent) => {
|
|
||||||
const button = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
|
|
||||||
if (!button || !current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
const target = current;
|
|
||||||
hide();
|
|
||||||
editor.undoManager.transact(() => {
|
|
||||||
editor.dom.remove(target);
|
|
||||||
});
|
|
||||||
editor.nodeChanged();
|
|
||||||
editor.undoManager.add();
|
|
||||||
if (editor.getBody().children.length === 0) {
|
|
||||||
editor.setContent('<p></p>');
|
|
||||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
|
||||||
}
|
|
||||||
syncEditorHeight(editor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncCodeLabels(editor: any): void {
|
|
||||||
const root: HTMLElement | null = editor?.getBody?.() ?? null;
|
|
||||||
if (!root) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
root.querySelectorAll<HTMLElement>('pre.enlighter-pre[data-jio-code]').forEach((pre) => {
|
|
||||||
const existing = pre.querySelector(':scope > .jio-code-lang');
|
|
||||||
const language = pre.getAttribute('data-enlighter-language') || 'generic';
|
|
||||||
if (existing) {
|
|
||||||
existing.setAttribute('data-jio-code-lang', language);
|
|
||||||
const label = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
|
||||||
if ((existing.textContent || '') !== label) {
|
|
||||||
existing.textContent = label;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const badge = document.createElement('span');
|
|
||||||
badge.className = 'jio-code-lang';
|
|
||||||
badge.contentEditable = 'false';
|
|
||||||
badge.setAttribute('data-jio-code-lang', language);
|
|
||||||
badge.textContent = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
|
||||||
pre.insertBefore(badge, pre.firstChild);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeAttribute(value: string): string {
|
function escapeAttribute(value: string): string {
|
||||||
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
@@ -355,7 +72,7 @@ function plainText(content: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Studio(): JSX.Element {
|
function Studio(): JSX.Element {
|
||||||
const [draft, setDraft] = useState<Draft>(initialDraft);
|
const [draft, setDraft] = useState<Draft>(() => initialDraft(bootstrap));
|
||||||
const draftRef = useRef(draft);
|
const draftRef = useRef(draft);
|
||||||
draftRef.current = draft;
|
draftRef.current = draft;
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
@@ -372,8 +89,6 @@ function Studio(): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
|
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
|
||||||
const [codeLanguage, setCodeLanguage] = useState<null | string>(null);
|
|
||||||
const codeTargetRef = useRef<any>(null);
|
|
||||||
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
||||||
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
||||||
const dirtyRef = useRef(false);
|
const dirtyRef = useRef(false);
|
||||||
@@ -881,18 +596,14 @@ function Studio(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="jio-creator" data-status={bootstrap.status || ''} data-editor-ready={editorReady ? 'true' : 'false'}>
|
<div className="jio-creator" data-status={bootstrap.status || ''} data-editor-ready={editorReady ? 'true' : 'false'}>
|
||||||
<header className="jio-creator__bar">
|
<div className="jio-creator__top">
|
||||||
<div className="jio-creator__bar-left">
|
<StudioMasthead
|
||||||
<a className="jio-creator__back" href={bootstrap.urls?.center || '/'} aria-label={__('返回作者中心', 'jio')}>
|
centerUrl={String(bootstrap.urls?.center || '')}
|
||||||
<i className="fa fas fa-angle-left" aria-hidden="true" />
|
site={bootstrap.site || {}}
|
||||||
</a>
|
viewer={bootstrap.viewer || {}}
|
||||||
<span className="jio-creator__bar-brand">Jio</span>
|
/>
|
||||||
<span className="jio-creator__bar-divider" aria-hidden="true" />
|
|
||||||
<span className="jio-creator__bar-mode">
|
|
||||||
{isPublishedEdit ? __('修改文章', 'jio') : __('写文章', 'jio')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<header className="jio-creator__bar">
|
||||||
<EditorToolbar
|
<EditorToolbar
|
||||||
ready={editorReady}
|
ready={editorReady}
|
||||||
formatValue={formatValue}
|
formatValue={formatValue}
|
||||||
@@ -908,6 +619,7 @@ function Studio(): JSX.Element {
|
|||||||
onInsertQuote={insertQuote}
|
onInsertQuote={insertQuote}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
{recovery && (
|
{recovery && (
|
||||||
<div className="jio-creator__notice" role="status">
|
<div className="jio-creator__notice" role="status">
|
||||||
@@ -962,48 +674,6 @@ function Studio(): JSX.Element {
|
|||||||
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{codeLanguage !== null && (
|
|
||||||
<div className="jio-creator-dialog" role="dialog" aria-modal="true" aria-labelledby="jio-code-lang-title">
|
|
||||||
<button type="button" className="jio-creator-dialog__backdrop" aria-label={__('关闭', 'jio')} onClick={() => setCodeLanguage(null)} />
|
|
||||||
<div className="jio-creator-dialog__panel">
|
|
||||||
<header className="jio-creator-dialog__head">
|
|
||||||
<h2 id="jio-code-lang-title">{__('代码语言', 'jio')}</h2>
|
|
||||||
<button type="button" className="jio-creator-dialog__close" onClick={() => setCodeLanguage(null)} aria-label={__('关闭', 'jio')}>
|
|
||||||
<i className="fa fas fa-times" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className="jio-creator-dialog__body">
|
|
||||||
<div className="jio-creator__lang-grid">
|
|
||||||
{CODE_LANGUAGES.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.value}
|
|
||||||
type="button"
|
|
||||||
className={`jio-creator__lang-item${item.value === codeLanguage ? ' is-active' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
const target = codeTargetRef.current;
|
|
||||||
codeTargetRef.current = null;
|
|
||||||
setCodeLanguage(null);
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target.setAttribute('data-enlighter-language', item.value);
|
|
||||||
const code = target.querySelector('code');
|
|
||||||
if (code) {
|
|
||||||
code.setAttribute('data-enlighter-language', item.value);
|
|
||||||
}
|
|
||||||
syncCodeLabels(editorRef.current);
|
|
||||||
updateContent(serializeBlocks(editorRef.current.getContent()));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{blockDialog && (
|
{blockDialog && (
|
||||||
<BlockDialog
|
<BlockDialog
|
||||||
kind={blockDialog}
|
kind={blockDialog}
|
||||||
|
|||||||
@@ -366,37 +366,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 代码语言选择 ---------- */
|
|
||||||
|
|
||||||
.jio-creator__lang-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__lang-item {
|
|
||||||
padding: 9px 6px;
|
|
||||||
border: 1px solid #e6e9e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
color: #4f5957;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
border-color: #b7cdc3;
|
|
||||||
color: var(--jc-accent-strong);
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
border-color: var(--jc-accent);
|
|
||||||
background: #eff6f3;
|
|
||||||
color: var(--jc-accent-strong);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 块级删除控件(接近 WPS 的「块操作」) ---------- */
|
/* ---------- 块级删除控件(接近 WPS 的「块操作」) ---------- */
|
||||||
|
|
||||||
.jio-creator__block-actions {
|
.jio-creator__block-actions {
|
||||||
@@ -454,8 +423,4 @@
|
|||||||
.jio-creator__status-primary {
|
.jio-creator__status-primary {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__lang-grid {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -48,13 +48,13 @@ body.jio-page-submit.has-static-nav {
|
|||||||
--jc-accent: #{$accent};
|
--jc-accent: #{$accent};
|
||||||
--jc-accent-strong: #{$accent-strong};
|
--jc-accent-strong: #{$accent-strong};
|
||||||
--jc-warn: #{$warn};
|
--jc-warn: #{$warn};
|
||||||
--jc-bar: 64px;
|
--jc-top: 112px;
|
||||||
--jc-status: 52px;
|
--jc-status: 52px;
|
||||||
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
/* 固定顶栏 / 底栏属于工作台 chrome,不随正文一起滚动。 */
|
/* 固定顶栏 / 底栏属于工作台 chrome,不随正文一起滚动。 */
|
||||||
padding-top: var(--jc-bar);
|
padding-top: var(--jc-top);
|
||||||
padding-bottom: var(--jc-status);
|
padding-bottom: var(--jc-status);
|
||||||
background: var(--jc-paper);
|
background: var(--jc-paper);
|
||||||
color: var(--jc-ink);
|
color: var(--jc-ink);
|
||||||
@@ -73,65 +73,41 @@ body.jio-page-submit.has-static-nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar {
|
/*
|
||||||
|
* 顶栏分两行:第一行是站点身份(左 LOGO / 右头像),第二行只有正文工具条。
|
||||||
|
* 两行合成一个固定容器,正文只需要预留一次高度。
|
||||||
|
*/
|
||||||
|
.jio-creator__top {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 300;
|
z-index: 300;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
|
||||||
height: var(--jc-bar);
|
|
||||||
align-items: center;
|
|
||||||
gap: clamp(10px, 1.7vw, 24px);
|
|
||||||
padding: 0 clamp(14px, 2.4vw, 30px);
|
|
||||||
border-bottom: 1px solid var(--jc-line);
|
|
||||||
background: rgba(250, 251, 250, 0.98);
|
background: rgba(250, 251, 250, 0.98);
|
||||||
box-shadow: 0 2px 8px rgba(27, 46, 39, 0.035);
|
box-shadow: 0 2px 8px rgba(27, 46, 39, 0.035);
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar-left,
|
.jio-creator__masthead {
|
||||||
.jio-creator__bar-actions {
|
display: flex;
|
||||||
|
height: 56px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 clamp(14px, 2.4vw, 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__masthead-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__bar-left {
|
|
||||||
padding-right: clamp(10px, 1.5vw, 20px);
|
|
||||||
border-right: 1px solid var(--jc-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__bar-actions {
|
|
||||||
margin-left: auto;
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: -0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__bar-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 18px;
|
|
||||||
background: var(--jc-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__bar-mode {
|
|
||||||
color: var(--jc-ink-soft);
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__back {
|
.jio-creator__back {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
flex: 0 0 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
@@ -146,10 +122,87 @@ body.jio-page-submit.has-static-nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jio-creator__brand {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(38, 118, 101, 0.35);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__brand-logo {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: 168px;
|
||||||
|
height: 30px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__brand-text {
|
||||||
|
overflow: hidden;
|
||||||
|
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: -0.01em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__me {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(38, 118, 101, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__me-avatar {
|
||||||
|
display: block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(24, 39, 34, 0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eef1f0;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&.is-placeholder {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #9aa2a0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__bar {
|
||||||
|
display: flex;
|
||||||
|
height: 56px;
|
||||||
|
align-items: center;
|
||||||
|
/* 工具条居中:两侧等宽留白,按钮组永远落在视口中线上。 */
|
||||||
|
justify-content: center;
|
||||||
|
/* 窄屏放不下时退回左对齐,否则居中会把最左侧的按钮挤到不可滚动区。 */
|
||||||
|
justify-content: safe center;
|
||||||
|
padding: 0 clamp(14px, 2.4vw, 30px);
|
||||||
|
border-top: 1px solid var(--jc-line);
|
||||||
|
border-bottom: 1px solid var(--jc-line);
|
||||||
|
}
|
||||||
|
|
||||||
.jio-creator__toolbar-tools {
|
.jio-creator__toolbar-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
max-width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -277,36 +330,6 @@ body.jio-page-submit.has-static-nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__save-draft {
|
|
||||||
display: inline-flex;
|
|
||||||
min-height: 34px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 7px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid var(--jc-line);
|
|
||||||
border-radius: 7px;
|
|
||||||
background: #fff;
|
|
||||||
color: var(--jc-ink-soft);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:hover:not(:disabled),
|
|
||||||
&:focus-visible:not(:disabled) {
|
|
||||||
border-color: #b7cdc3;
|
|
||||||
background: #f7faf8;
|
|
||||||
color: var(--jc-accent-strong);
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__save {
|
.jio-creator__save {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -341,48 +364,6 @@ body.jio-page-submit.has-static-nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__ghost,
|
|
||||||
.jio-creator__primary,
|
|
||||||
.jio-creator__btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 0 13px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 7px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__ghost {
|
|
||||||
border-color: var(--jc-line);
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
border-color: var(--jc-accent);
|
|
||||||
color: var(--jc-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__primary {
|
|
||||||
background: var(--jc-accent);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
background: var(--jc-accent-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__notice {
|
.jio-creator__notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -697,31 +678,32 @@ body.jio-page-submit.has-static-nav {
|
|||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.jio-creator {
|
.jio-creator {
|
||||||
/* 图标 + 文字的两行按钮把顶栏撑高,移动端同样保留文字。 */
|
/* 两行顶栏在手机端压缩:账号行 50px、工具条 52px。 */
|
||||||
--jc-bar: 60px;
|
--jc-top: 102px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar {
|
.jio-creator__masthead {
|
||||||
gap: 7px;
|
height: 50px;
|
||||||
padding: 0 8px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar-left {
|
.jio-creator__brand-logo {
|
||||||
gap: 5px;
|
max-width: 128px;
|
||||||
padding-right: 7px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar-mode,
|
.jio-creator__brand-text {
|
||||||
.jio-creator__bar-divider {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__bar-brand {
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar-actions {
|
.jio-creator__me-avatar {
|
||||||
gap: 5px;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-creator__bar {
|
||||||
|
height: 52px;
|
||||||
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__toolbar-tools {
|
.jio-creator__toolbar-tools {
|
||||||
@@ -756,37 +738,6 @@ body.jio-page-submit.has-static-nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jio-creator__bar-actions .jio-creator__save {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__save-draft {
|
|
||||||
width: 34px;
|
|
||||||
min-width: 34px;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 0;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__preview {
|
|
||||||
min-height: 32px;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__primary {
|
|
||||||
min-height: 32px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__ghost {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jio-creator__canvas {
|
.jio-creator__canvas {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
margin: 14px auto 0;
|
margin: 14px auto 0;
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ final class CreatorRest {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
$terms = $this->term_payload($settings, $source, $snapshot);
|
$terms = $this->term_payload($settings, $source, $snapshot);
|
||||||
|
$viewer = $user_id > 0 ? get_userdata($user_id) : null;
|
||||||
|
$ident = $this->identity_payload($viewer instanceof \WP_User ? $viewer : null);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'sourceId' => $post_id,
|
'sourceId' => $post_id,
|
||||||
@@ -172,6 +174,40 @@ final class CreatorRest {
|
|||||||
'newPost' => function_exists('jio_get_new_post_url') ? jio_get_new_post_url() : '',
|
'newPost' => function_exists('jio_get_new_post_url') ? jio_get_new_post_url() : '',
|
||||||
'center' => function_exists('jio_get_user_center_url') ? jio_get_user_center_url('posts') : '',
|
'center' => function_exists('jio_get_user_center_url') ? jio_get_user_center_url('posts') : '',
|
||||||
],
|
],
|
||||||
|
'site' => $ident['site'],
|
||||||
|
'viewer' => $ident['viewer'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶栏身份信息:站点 Logo 与当前用户头像。
|
||||||
|
*
|
||||||
|
* 只做展示,不承载权限判断;Logo 缺失时前端回退到品牌文字。
|
||||||
|
*
|
||||||
|
* @return array{site: array<string, string>, viewer: array<string, string>}
|
||||||
|
*/
|
||||||
|
private function identity_payload(?\WP_User $viewer = null): array {
|
||||||
|
$brand = function_exists('jio_get_site_logo_text') ? (string) jio_get_site_logo_text() : '';
|
||||||
|
$brand = $brand !== '' ? $brand : (string) get_bloginfo('name');
|
||||||
|
|
||||||
|
$logo = function_exists('jio_get_site_logo_url') ? trim((string) jio_get_site_logo_url()) : '';
|
||||||
|
if ($logo === '') {
|
||||||
|
$logo = get_template_directory_uri() . '/assets/img/logo.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = $viewer instanceof \WP_User && function_exists('jio_get_avatar_url')
|
||||||
|
? (string) jio_get_avatar_url($viewer, ['size' => 72])
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'site' => [
|
||||||
|
'home' => esc_url_raw(home_url('/')),
|
||||||
|
'name' => $brand,
|
||||||
|
'logo' => esc_url_raw($logo),
|
||||||
|
],
|
||||||
|
'viewer' => [
|
||||||
|
'avatar' => esc_url_raw($avatar),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,8 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
|
|||||||
assert.match(blocks, /function append_footnotes/);
|
assert.match(blocks, /function append_footnotes/);
|
||||||
assert.match(blocks, /<sup>\$1\$2<\/sup>/);
|
assert.match(blocks, /<sup>\$1\$2<\/sup>/);
|
||||||
// 删除键必须能移除相邻的块级元素。
|
// 删除键必须能移除相邻的块级元素。
|
||||||
assert.match(appSource, /function removeAdjacentBlock/);
|
const editorRuntime = read('assets/creator-studio/src/editor-runtime.ts');
|
||||||
|
assert.match(editorRuntime, /function removeAdjacentBlock/);
|
||||||
assert.match(appSource, /removeAdjacentBlock\(editor, event\)/);
|
assert.match(appSource, /removeAdjacentBlock\(editor, event\)/);
|
||||||
// 工具栏按钮统一为「图标在上、文字在下」。
|
// 工具栏按钮统一为「图标在上、文字在下」。
|
||||||
assert.match(editorToolbar, /jio-creator__tool-label/);
|
assert.match(editorToolbar, /jio-creator__tool-label/);
|
||||||
@@ -231,8 +232,37 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
|
|||||||
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?position:\s*fixed/);
|
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?position:\s*fixed/);
|
||||||
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?bottom:\s*0/);
|
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?bottom:\s*0/);
|
||||||
assert.match(editorToolbar, /role="toolbar"/);
|
assert.match(editorToolbar, /role="toolbar"/);
|
||||||
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__bar\s*\{[^}]*position:\s*fixed/);
|
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__top\s*\{[^}]*position:\s*fixed/);
|
||||||
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator\s*\{[\s\S]*?padding-top:\s*var\(--jc-bar\)/);
|
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator\s*\{[\s\S]*?padding-top:\s*var\(--jc-top\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('top bar is masthead + centered toolbar, with no brand text in the toolbar row', () => {
|
||||||
|
const masthead = read('assets/creator-studio/src/components/studio-masthead.tsx');
|
||||||
|
const styles = read('assets/creator-studio/src/styles/studio.scss');
|
||||||
|
const rest = read('src/Core/CreatorRest.php');
|
||||||
|
|
||||||
|
// 第一行:左站点 LOGO、右用户头像。
|
||||||
|
assert.match(appSource, /<StudioMasthead/);
|
||||||
|
assert.match(masthead, /jio-creator__masthead/);
|
||||||
|
assert.match(masthead, /jio-creator__brand-logo/);
|
||||||
|
assert.match(masthead, /jio-creator__me-avatar/);
|
||||||
|
assert.match(masthead, /site\.logo/);
|
||||||
|
assert.match(masthead, /viewer\.avatar/);
|
||||||
|
// 工具条行不再有品牌字 / 模式文案。
|
||||||
|
assert.doesNotMatch(appSource, /jio-creator__bar-brand/);
|
||||||
|
assert.doesNotMatch(appSource, /jio-creator__bar-mode/);
|
||||||
|
assert.doesNotMatch(appSource, /jio-creator__bar-left/);
|
||||||
|
// 工具条居中。
|
||||||
|
assert.match(styles, /\.jio-creator__bar\s*\{[^}]*justify-content:\s*center/);
|
||||||
|
// Logo 与头像由服务端 bootstrap 提供,不在前端拼 URL。
|
||||||
|
assert.match(rest, /private function identity_payload/);
|
||||||
|
assert.match(rest, /'site'\s*=>\s*\$ident\['site'\]/);
|
||||||
|
assert.match(rest, /'viewer'\s*=>\s*\$ident\['viewer'\]/);
|
||||||
|
assert.match(rest, /jio_get_site_logo_url/);
|
||||||
|
assert.match(rest, /jio_get_avatar_url/);
|
||||||
|
// 顶栏身份字段与前端读取保持一致,避免悬空字段。
|
||||||
|
assert.doesNotMatch(read('assets/creator-studio/src/components/studio-masthead.tsx'), /viewer\.home/);
|
||||||
|
assert.doesNotMatch(rest, /'home'\s*=>\s*esc_url_raw\(\$home\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creator studio build artifacts and contracts exist', () => {
|
test('creator studio build artifacts and contracts exist', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user