feat(console): background selector plain/glow/photo with upload
This commit is contained in:
@@ -71,6 +71,8 @@ type ViewState = {
|
||||
consoleCols?: number
|
||||
/** 控制室卡片形状:square/circle(默认 square)*/
|
||||
consoleShape?: 'square' | 'circle'
|
||||
/** 控制室背景:plain/glow/photo(默认 glow)*/
|
||||
consoleBg?: 'plain' | 'glow' | 'photo'
|
||||
}
|
||||
|
||||
/** 卡片上报的项目元信息(协议 v2)。 */
|
||||
@@ -298,6 +300,7 @@ function loadView(): ViewState {
|
||||
consoleTheme: p.consoleTheme === 'dark' || p.consoleTheme === 'light' || p.consoleTheme === 'system' ? p.consoleTheme : 'system',
|
||||
consoleCols: typeof p.consoleCols === 'number' && p.consoleCols >= 1 && p.consoleCols <= 5 ? Math.round(p.consoleCols) : 3,
|
||||
consoleShape: p.consoleShape === 'circle' ? 'circle' : 'square',
|
||||
consoleBg: p.consoleBg === 'plain' || p.consoleBg === 'photo' ? p.consoleBg : 'glow',
|
||||
}
|
||||
} catch {
|
||||
return { ...DEFAULT_VIEW }
|
||||
@@ -1340,6 +1343,10 @@ function WorktableSection(props: any) {
|
||||
setCols: (n: number) => persistView({ consoleCols: n }),
|
||||
getShape: () => viewRef.current.consoleShape ?? 'square',
|
||||
setShape: (s: 'square' | 'circle') => persistView({ consoleShape: s }),
|
||||
getBg: () => viewRef.current.consoleBg ?? 'glow',
|
||||
setBg: (m: 'plain' | 'glow' | 'photo') => persistView({ consoleBg: m }),
|
||||
getPhoto: () => { try { return localStorage.getItem('dsh.worktable.consoleBgPhoto.v1') ?? '' } catch { return '' } },
|
||||
setPhoto: (dataUrl: string) => { try { localStorage.setItem('dsh.worktable.consoleBgPhoto.v1', dataUrl) } catch {} },
|
||||
onAck: (id) => { ackRef.current?.(id); setNotifyTick((t) => t + 1); notifyConsole() },
|
||||
refreshPreviews: () => {
|
||||
if (previewTimer != null) { window.clearTimeout(previewTimer); previewTimer = null }
|
||||
|
||||
@@ -119,6 +119,10 @@ export const zh = {
|
||||
'console.cols': '每行',
|
||||
'console.colsLabel': '每行显示项目数',
|
||||
'console.shapeLabel': '卡片形状',
|
||||
'console.bgLabel': '背景',
|
||||
'console.bgPlain': '纯色背景',
|
||||
'console.bgGlow': '流光背景',
|
||||
'console.bgPhoto': '上传照片',
|
||||
'console.shapeSquare': '方形',
|
||||
'console.shapeCircle': '圆形',
|
||||
'console.themeDark': '深色',
|
||||
@@ -302,6 +306,10 @@ export const en = {
|
||||
'console.cols': 'Per row',
|
||||
'console.colsLabel': 'Projects per row',
|
||||
'console.shapeLabel': 'Card shape',
|
||||
'console.bgLabel': 'Background',
|
||||
'console.bgPlain': 'Plain',
|
||||
'console.bgGlow': 'Glow',
|
||||
'console.bgPhoto': 'Photo',
|
||||
'console.shapeSquare': 'Square',
|
||||
'console.shapeCircle': 'Circle',
|
||||
'console.themeDark': 'Dark',
|
||||
|
||||
@@ -1018,6 +1018,8 @@ function ConsolePane() {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const [cols, setColsState] = useState<number>(() => splitEnv?.console?.getCols?.() ?? 3)
|
||||
const [shape, setShapeState] = useState<'square' | 'circle'>(() => splitEnv?.console?.getShape?.() ?? 'square')
|
||||
const [bg, setBgState] = useState<'plain' | 'glow' | 'photo'>(() => splitEnv?.console?.getBg?.() ?? 'glow')
|
||||
const [bgPhoto, setBgPhoto] = useState<string>(() => splitEnv?.console?.getPhoto?.() ?? '')
|
||||
const gridRef = useRef<HTMLDivElement | null>(null)
|
||||
const firstRectsRef = useRef<Map<string, { x: number; y: number }> | null>(null)
|
||||
const onCols = (n: number) => {
|
||||
@@ -1038,6 +1040,42 @@ function ConsolePane() {
|
||||
setShapeState(s)
|
||||
splitEnv?.console?.setShape?.(s)
|
||||
}
|
||||
const onBg = (m: 'plain' | 'glow' | 'photo') => {
|
||||
if (m === 'photo') {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
const url = URL.createObjectURL(file)
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const maxDim = 1600
|
||||
const scale = Math.min(1, maxDim / Math.max(img.width, img.height))
|
||||
const w = Math.max(1, Math.round(img.width * scale))
|
||||
const h = Math.max(1, Math.round(img.height * scale))
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = w
|
||||
canvas.height = h
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) { URL.revokeObjectURL(url); return }
|
||||
ctx.drawImage(img, 0, 0, w, h)
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85)
|
||||
URL.revokeObjectURL(url)
|
||||
setBgPhoto(dataUrl)
|
||||
setBgState('photo')
|
||||
splitEnv?.console?.setBg?.('photo')
|
||||
splitEnv?.console?.setPhoto?.(dataUrl)
|
||||
}
|
||||
img.src = url
|
||||
}
|
||||
input.click()
|
||||
return
|
||||
}
|
||||
setBgState(m)
|
||||
splitEnv?.console?.setBg?.(m)
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
const first = firstRectsRef.current
|
||||
if (!first) return
|
||||
@@ -1114,8 +1152,13 @@ function ConsolePane() {
|
||||
]
|
||||
return (
|
||||
<div className="dsh-wt_console" data-wt-theme={resolvedTheme} data-wt-shape={shape}>
|
||||
<span className="dsh-wt_consoleBg" aria-hidden><i className="dsh-wt_blob dsh-wt_blob1" /><i className="dsh-wt_blob dsh-wt_blob2" /><i className="dsh-wt_blob dsh-wt_blob3" /><i className="dsh-wt_blob dsh-wt_blob4" /></span>
|
||||
<span className="dsh-wt_consoleBg" aria-hidden style={bg === 'photo' && bgPhoto ? { backgroundImage: 'url("' + bgPhoto + '")', backgroundSize: 'cover', backgroundPosition: 'center' } : undefined}>{bg === 'glow' && (<><i className="dsh-wt_blob dsh-wt_blob1" /><i className="dsh-wt_blob dsh-wt_blob2" /><i className="dsh-wt_blob dsh-wt_blob3" /><i className="dsh-wt_blob dsh-wt_blob4" /></>)}</span>
|
||||
<div className="dsh-wt_consoleHead">
|
||||
<div className="dsh-wt_consoleBgBtns" role="group" aria-label={T('console.bgLabel')}>
|
||||
<button type="button" className={'dsh-wt_consoleBgBtn' + (bg === 'plain' ? ' dsh-wt_consoleBgBtnOn' : '')} title={T('console.bgPlain')} aria-label={T('console.bgPlain')} onClick={() => onBg('plain')}><svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><rect x="3" y="3" width="10" height="10" rx="2" fill="currentColor" /></svg></button>
|
||||
<button type="button" className={'dsh-wt_consoleBgBtn' + (bg === 'glow' ? ' dsh-wt_consoleBgBtnOn' : '')} title={T('console.bgGlow')} aria-label={T('console.bgGlow')} onClick={() => onBg('glow')}><svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><circle cx="8" cy="8" r="3" fill="currentColor" /><circle cx="8" cy="8" r="5.5" fill="none" stroke="currentColor" strokeWidth="1" opacity=".5" /></svg></button>
|
||||
<button type="button" className={'dsh-wt_consoleBgBtn' + (bg === 'photo' ? ' dsh-wt_consoleBgBtnOn' : '')} title={T('console.bgPhoto')} aria-label={T('console.bgPhoto')} onClick={() => onBg('photo')}><svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><rect x="2.5" y="3.5" width="11" height="9" rx="1.5" fill="none" stroke="currentColor" strokeWidth="1.2" /><circle cx="5.8" cy="6.6" r="1.1" fill="currentColor" /><path d="M3.2 11.2l2.8-2.8 2.2 2.2 1.8-1.8 2.8 2.4" fill="none" stroke="currentColor" strokeWidth="1.2" /></svg></button>
|
||||
</div>
|
||||
<div className="dsh-wt_consoleShape" role="group" aria-label={T('console.shapeLabel')}>
|
||||
<button type="button" className={'dsh-wt_consoleShapeBtn' + (shape === 'square' ? ' dsh-wt_consoleShapeBtnOn' : '')} title={T('console.shapeSquare')} aria-label={T('console.shapeSquare')} onClick={() => onShape('square')}>
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><rect x="4" y="4" width="8" height="8" rx="1.5" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
|
||||
|
||||
@@ -426,6 +426,10 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_consoleShapeBtn{flex:none;width:26px;height:26px;padding:0;border:none;border-radius:999px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;opacity:.85}',
|
||||
'.dsh-wt_consoleShapeBtn:hover{opacity:1;background:var(--wt-chip)}',
|
||||
'.dsh-wt_consoleShapeBtnOn{background:var(--wt-chip);color:var(--wt-text);opacity:1}',
|
||||
'.dsh-wt_consoleBgBtns{display:inline-flex;align-items:center;padding:2px;border:1px solid var(--wt-border);border-radius:999px;background:var(--wt-card)}',
|
||||
'.dsh-wt_consoleBgBtn{flex:none;width:26px;height:26px;padding:0;border:none;border-radius:999px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;opacity:.85}',
|
||||
'.dsh-wt_consoleBgBtn:hover{opacity:1;background:var(--wt-chip)}',
|
||||
'.dsh-wt_consoleBgBtnOn{background:var(--wt-chip);color:var(--wt-text);opacity:1}',
|
||||
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleGrid{max-width:calc(var(--wt-cols,3)*268px + (var(--wt-cols,3) - 1)*64px)}',
|
||||
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleCard{border-radius:50%;justify-content:center;align-items:center;padding:24px 24px 38px;gap:9px}',
|
||||
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleCardHead{width:100%;flex-direction:column;justify-content:center;gap:5px;text-align:center}',
|
||||
|
||||
Reference in New Issue
Block a user