Skip to content

Commit

Permalink
Merge branch 'dev.upload'
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Oct 23, 2024
2 parents 22e14b0 + 9a57829 commit 8ee3d2e
Show file tree
Hide file tree
Showing 28 changed files with 830 additions and 123 deletions.
6 changes: 5 additions & 1 deletion admin/src/app/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export function buildContext(opt: Required<buildOptions>, f: API) {

const r = await f.get<User>(opt.api.info);
if (r.ok) {
return r.body as User;
const u = r.body;
if (u && !u.avatar) {
u.avatar = opt.logo;
}
return u;
}

await window.notify(r.body!.title);
Expand Down
2 changes: 1 addition & 1 deletion admin/src/app/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function Username(): JSX.Element {

const activator = <Button class="pl-1 rounded-full"
onClick={()=>setVisible(!visible())}>
<img class="w-6 h-6 rounded-full" src={ ctx.user()?.avatar ?? opt.logo } />
<img class="w-6 h-6 rounded-full mr-1" src={ ctx.user()?.avatar } />
{ctx.user()?.name}
</Button>;

Expand Down
16 changes: 8 additions & 8 deletions admin/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export default function(props: Props) {
const [_, btnProps] = splitProps(props, ['kind', 'rounded', 'palette', 'icon', 'children', 'classList']);

return <button {...btnProps} classList={{
'c--button': true,
'c--icon': props.icon,
'c--button-icon': props.icon,
[`c--button-${props.kind}`]: true,
[`palette--${props.palette}`]: !!props.palette,
'rounded-full': props.rounded,
...props.classList
}}>
'c--button': true,
'c--icon': props.icon,
'c--button-icon': props.icon,
[`c--button-${props.kind}`]: true,
[`palette--${props.palette}`]: !!props.palette,
'rounded-full': props.rounded,
...props.classList
}}>
{props.children}
</button>;
}
1 change: 0 additions & 1 deletion admin/src/components/form/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT


import { useApp } from '@/app/context';
import { Demo, paletteSelector } from '@/components/base/demo';
import { Button } from '../button';
Expand Down
2 changes: 1 addition & 1 deletion admin/src/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ export * from './editor';
export * from './radio';
export * from './textarea';
export * from './textfield';

export * from './upload';
1 change: 1 addition & 0 deletions admin/src/components/form/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@import './editor/style.css';
@import './textfield/style.css';
@import './date/style.css';
@import './upload/style.css';

@layer components {
.c--form {
Expand Down
115 changes: 115 additions & 0 deletions admin/src/components/form/upload/album.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

import { createMemo, For, JSX, mergeProps, onMount, Show } from 'solid-js';

import { Accessor } from '@/components/form';
import { PreviewFile, PreviewURL } from './preview';
import Upload, { Props as BaseProps, Ref } from './upload';

export interface Props extends Omit<BaseProps,'dropzone'|'ref'> {
/**
* 是否接受直接拖入文件
*
* 非响应式的属性
*/
droppable?: boolean;

/**
* 是否自动执行上传操作
*/
auto?: boolean;

/**
* 逆向显示内容,这将会导致上传按钮显示在最前面。
*/
reverse?: boolean;

/**
* 保存着所有已经上传的文件列表
*/
accessor: Accessor<Array<string>>;

/**
* 子项的宽度
*/
itemSize?: string;
}

const presetProps: Readonly<Partial<Props>> = {
itemSize: '72px',
};

export default function(props: Props): JSX.Element {
props = mergeProps(presetProps, props);
const access = props.accessor;

let dropRef: HTMLDivElement;
let uploadRef: Ref;

onMount(()=>{
if (!props.droppable) {
dropRef.addEventListener('dragover', (e)=>{
e.dataTransfer!.dropEffect = 'none';
e.preventDefault();
});
return;
}
});

const size = createMemo((): JSX.CSSProperties => {
return { 'height': props.itemSize, 'width': props.itemSize };
});

return <fieldset disabled={props.disabled} class={props.class} classList={{
...props.classList,
'c--upload': true,
'c--field': true,
[`palette--${props.palette}`]: !!props.palette,
}}>
<div>
{props.label}
<div ref={el => dropRef = el} classList={{
'content': true,
}}>
<Upload ref={el => uploadRef = el} fieldName={props.fieldName} multiple={props.multiple} action={props.action}
accept={props.accept} dropzone={dropRef!} />

<For each={access.getValue()}>
{(item) => (
<PreviewURL size={props.itemSize!} url={item} del={() => {
access.setValue(access.getValue().filter((v) => v !== item));
}} />
)}
</For>

<For each={uploadRef!.files()}>
{(item, index) => {
return <PreviewFile size={props.itemSize!} file={item} del={() => {
uploadRef.delete(index());
}} />;
}}
</For>
<Show when={props.auto && (props.multiple || (access.getValue().length + uploadRef!.files().length) === 0)}>
<button style={size()} class={'c--icon action' + (props.reverse ? ' start' : '')} onClick={async () => {
uploadRef.pick();
await uploadRef.upload();
}}>upload_file</button>
</Show>
<Show when={!props.auto}>
<Show when={(props.multiple || (access.getValue().length + uploadRef!.files().length) === 0)}>
<button style={size()} class={'c--icon action' + (props.reverse ? ' start' : '')} onClick={() => uploadRef.pick()}>add</button>
</Show>
<Show when={uploadRef!.files().length > 0}>
<button style={size()} class={'c--icon action' + (props.reverse ? ' start' : '')} onClick={() => uploadRef!.upload()}>upload</button>
</Show>
</Show>
</div>
</div>

<Show when={access.hasError()}>
<p class="field_error" role="alert">{access.getError()}</p>
</Show>
</fieldset>;
}
43 changes: 43 additions & 0 deletions admin/src/components/form/upload/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

import { JSX } from 'solid-js';

import { useOptions } from '@/app/context';
import { boolSelector, Demo, paletteSelector, Stage } from '@/components/base/demo';
import { FieldAccessor } from '../access';
import { default as Album } from './album';

export default function(): JSX.Element {
const opt = useOptions();

const [paletteS, palette] = paletteSelector('secondary');
const [disabledS, disabled] = boolSelector('disabled');
const [reverseS, reverse] = boolSelector('reverse');
const [autoS, auto] = boolSelector('auto');

const basicA = FieldAccessor('upload', [opt.logo, './test.jpg'], true);

return <Demo settings={
<>
{paletteS}
{disabledS}
{reverseS}
{autoS}
<button class="c--button c--button-fill palette--primary" onClick={() => basicA.setError(basicA.getError() ? undefined : 'error')}>toggle error</button>
</>
} stages={
<>
<Stage title='basic'>
<Album label="label" class='min-w-16' reverse={reverse()} disabled={disabled()} palette={palette()} auto={auto()}
action='./' accessor={basicA} />
</Stage>

<Stage title='basic+drop'>
<Album class='min-w-16' reverse={reverse()} disabled={disabled()} palette={palette()} droppable auto={auto()}
action='./' accessor={basicA} />
</Stage>
</>
} />;
}
11 changes: 11 additions & 0 deletions admin/src/components/form/upload/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

export { default as Upload } from './upload';
export type { Props as UploadProps, Ref as UploadRef } from './upload';

export { default as Album } from './album';
export type { Props as AlbumProps } from './album';

export { file2Base64 } from './preview';
79 changes: 79 additions & 0 deletions admin/src/components/form/upload/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

import { createEffect, createSignal, JSX } from 'solid-js';

import { Icon } from '@/components/icon';

export interface URLProps {
size: string;
url: string;
del: {():void;};
}

/**
* 根据 URL 生成的预览图
*/
export function PreviewURL(props: URLProps): JSX.Element {
return <div class="preview" style={{
'width': props.size,
'height': props.size,
'background-image': isImageURL(props.url) ? props.url : '',
'background-size': '100% 100%',
}}>
<Icon class="close" icon="close" onClick={props.del} />
</div>;
}

export interface FileProps {
size: string;
file: File;
del: {():void;};
}

/**
* 根据 {@link File} 生成的预览图
*/
export function PreviewFile(props: FileProps): JSX.Element {
const [bg, setBG] = createSignal<string>('');

createEffect(async () => {
if (props.file.type.startsWith('image/')) {
setBG('url("'+await file2Base64(props.file) as string)+'")';
} else {
setBG('');
}
});

return <div class="preview" style={{
'width': props.size,
'height': props.size,
'background-image': bg(),
'background-size': '100% 100%',
}}>
<Icon class="close" icon="close" onClick={props.del} />
</div>;
}

export function file2Base64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
});
}

const imageExts: ReadonlyArray<string> = [
'.jpg','.jpeg','.png','.bmp','.ico','.svg',
];

function isImageURL(url: string): boolean {
const index = url.lastIndexOf('.');
if (index === -1 || (index === url.length - 1)) {
return false;
}

return imageExts.includes(url.slice(index));
}
58 changes: 58 additions & 0 deletions admin/src/components/form/upload/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2024 caixw
*
* SPDX-License-Identifier: MIT
*/

@layer components {
.c--upload {
.content {
@apply w-fit h-fit border rounded-md border-palette-fg-low;
@apply flex flex-wrap gap-2 p-2 items-center justify-start content-start;

.preview {
@apply relative border border-palette-bg-low p-1 rounded-sm order-10;

.close {
@apply absolute right-1 top-1 text-palette-fg;
@apply hover:text-palette-fg-high hover:cursor-pointer;
}
}

.action {
@apply relative border border-palette-bg-low p-1 rounded-sm order-11;
@apply text-2xl text-palette-fg-low;
}

.action.start {
@apply !order-1;
}
}
}

.c--upload:enabled {
.content {
.preview {
@apply hover:border-palette-fg-low;
}

.action {
@apply hover:enabled:border-palette-fg-low hover:disabled:cursor-not-allowed;
}
}
}

.c--upload:disabled {
.content {
@apply cursor-not-allowed text-palette-bg-low border-palette-bg-low;

.preview {
@apply cursor-not-allowed;
}

.action {
@apply cursor-not-allowed;
}
}
}
}
Loading

0 comments on commit 8ee3d2e

Please sign in to comment.