UNPKG

@ithinkdt/naive

Version:

iThinkDT Naive UI

612 lines (575 loc) 22.5 kB
import { defineComponent, withDirectives, resolveDirective, computed, reactive, h } from 'vue' import { asyncComputed } from '@vueuse/core' import { measureText } from '@ithinkdt/common' import { i18n, useTheme } from '@ithinkdt/core' import { commonDark, commonLight, NAvatarGroup, NAvatar, NH4, NPopover, NButton, NCard, NDropdown, NFlex, NSpin, NTag, NText, NTooltip, formProps, cardProps, NScrollbar, NConfigProvider, lightTheme, NIcon, } from 'ithinkdt-ui' import { DtForm, NFiles } from '../components/index.js' import { flexDirCol, cB, cE, cM, c, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p } from '@ithinkdt/core/cssr' import { vTooltip, vSpin } from '../directives' import { message, dialog, notification } from './provider' import { IAccount } from './assets.jsx' const darkTheme = asyncComputed(() => import('ithinkdt-ui/es/themes/dark').then((m) => m.darkTheme)) export const ThemeProvider = defineComponent({ name: 'DtThemeProvider', props: { theme: { type: String, default: 'auto' }, }, setup(props, { slots }) { const theme = useTheme() const darkThemeOverrides = computed(() => ({ ...theme._naiveThemeOverrides(true), common: { ...commonDark, ...theme._vars(true) }, })) const lightThemeOverrides = computed(() => ({ ...theme._naiveThemeOverrides(false), common: { ...commonLight, ...theme._vars(false) }, })) return () => h( NConfigProvider, { abstract: true, preflightStyleDisabled: true, theme: props.theme === 'dark' ? darkTheme.value : props.theme === 'light' ? lightTheme : undefined, themeOverrides: props.theme === 'dark' ? darkThemeOverrides.value : props.theme === 'light' ? lightThemeOverrides.value : undefined, }, slots, ) }, }) export function naive(options) { const themeOverrides = reactive({}) const vars = (isDark) => { if (options.theme.naiveThemeOverrides) { for (const k of Object.keys(themeOverrides)) delete themeOverrides[k] Object.assign(themeOverrides, options.theme.naiveThemeOverrides(isDark)) } const vars = { ...(isDark ? commonDark : commonLight), ...options.theme.vars?.(isDark), name: undefined, } delete vars['name'] return vars } const renderTableBtn = ({ text, title, type, dropdown, onClick, model: _0, index: _1, ...btn }) => { let _btn = ( <NButton {...btn} key={text} text type={type ?? 'primary'} onClick={dropdown ? undefined : onClick} style="line-height: inherit" > {text} </NButton> ) if (dropdown) { _btn = ( <NDropdown keyField="value" options={dropdown} onSelect={(cmd) => { onClick(cmd) }} > {_btn} </NDropdown> ) } return title ? ( <NTooltip> {{ default: () => title, trigger: () => _btn, }} </NTooltip> ) : ( _btn ) } const moreWidth = computed(() => measureText(i18n.t('table.op.more'))) // USER const renderUser = (user, showName, { placement, size }) => { const name = user.nickname.split(' ').at(-1) let text = name if (/^[\u4E00-\u9FA5]+$/.test(name)) { // 全中文 const l = name.length text = l >= 3 ? name.slice(-2) : name } else { if (name.length > 4) { text = ( <NIcon> <IAccount /> </NIcon> ) } } return ( <NPopover key={user.username} raw placement={placement}> {{ default: () => { return ( options.renderUserPopover?.(user) || ( <NCard style="width: 200px" size="small"> <NH4>{user.nickname}</NH4> <span>账号:{user.username}</span> </NCard> ) ) }, trigger: () => { const avatar = ( <NAvatar color={`var(--${p}-primary-color)`} round size={size} style={showName ? { position: 'absolute', bottom: -(size - 20) / 2 + 'px' } : ''} > {text} </NAvatar> ) return showName ? ( <div style={ showName ? { display: 'inline-block', minWidth: size + 'px', height: size + 'px', } : '' } > {avatar} {showName ? ( <span style={{ marginLeft: size + 6 + 'px' }}>{user.nickname}</span> ) : undefined} </div> ) : ( avatar ) }, }} </NPopover> ) } return { install() { // not empty }, directives: { tooltip: vTooltip, spin: vSpin, }, views: { Login: options.Login, Error: options.Error, Logout: options.Logout, }, theme: { topbarDark: false, sidebarDark: false, hasFooter: false, logoutPlacement: 'dropdown', usernamePlacement: 'outlet', showChangePwd: true, menuIconLoader: undefined, accordionMenu: false, ...options.theme, vars, naiveThemeOverrides: themeOverrides, watermark: undefined, _vars: options.theme.vars, _naiveThemeOverrides: options.theme.naiveThemeOverrides, _watermark: options.theme.watermark || undefined, persistExcludesKeys: [ 'logoPlacement', 'logoutPlacement', 'showChangePwd', 'naiveThemeOverrides', 'menuIconLoader', 'watermark', '_vars', '_naiveThemeOverrides', '_watermark', ...(options.theme.persistExcludesKeys ?? []), ], }, feedback: { messageApi: handleFeedbackApi( (content, options) => message.create(content, options), () => message, ['info', 'success', 'warning', 'error', 'loading'], ['destroyAll'], 1, ), dialogApi: handleFeedbackApi( (options) => { const $ret = dialog.create({ // eslint-disable-next-line unicorn/no-null positiveText: options.okText === undefined ? '确 定' : options.okText || null, // eslint-disable-next-line unicorn/no-null negativeText: options.cancelText === undefined ? '取 消' : options.cancelText || null, onAfterLeave: () => { $ret.loading = false options.onAfterLeave?.() }, onPositiveClick: async (e) => { if ($ret.loading) return false $ret.loading = true const { maskClosable, closable, closeOnEsc, negativeButtonProps } = $ret $ret.maskClosable = false $ret.closable = false $ret.closeOnEsc = false if ($ret.cancelText === undefined) { if ($ret.negativeButtonProps) { $ret.negativeButtonProps.disabled = true } else { $ret.negativeButtonProps = { disabled: true, } } } try { const ret = await options.onOk?.(e) if (ret === false) { $ret.loading = false } return ret } catch (error) { $ret.loading = false throw error } finally { $ret.maskClosable = maskClosable $ret.closable = closable $ret.closeOnEsc = closeOnEsc if ($ret.cancelText === undefined) { $ret.negativeButtonProps = negativeButtonProps } } }, onNegativeClick: async (e) => { if ($ret.cancelText === undefined) { if ($ret.loading) return false if ((await options.onClose?.()) === false) { return false } } return options.onCancel?.(e) }, ...options, }) return $ret }, () => dialog, ['info', 'success', 'warning', 'error'], ['destroyAll'], ), notificationApi: handleFeedbackApi( (options) => notification.create(options), () => notification, ['info', 'success', 'warning', 'error'], ['destroyAll'], ), }, page: { handleFormModalOptions: (options) => { let modal = { wrap: false, }, form = {} for (const k of Object.keys(options)) { if (k in formProps || k in cardProps) { form[k] = options[k] } else { modal[k] = options[k] } } return { modal, form } }, renderFormModal: ({ type, onClose, title, submiting, formRef, onSubmit, btns, submitText: _1, cancelText: _2, contentClass, contentStyle = {}, headerClass, headerStyle, headerExtraClass, headerExtraStyle, footerClass, footerStyle, ...binds }) => { createStyle(`${p}-form-modal`) const cardProps = { contentClass, headerClass, headerStyle, headerExtraClass, headerExtraStyle, footerClass, footerStyle, } const vTooltip = resolveDirective('tooltip') return ( <NCard segmented={{ content: true, action: true }} closable={!submiting} closeOnEsc={!submiting} onClose={onClose} size="small" style="height: 100%" {...cardProps} contentStyle="height: 100%; overflow: hidden" > {{ header: () => title, default: () => ( <div class={[`${p}-form-modal`, type === 'drawer' ? `${p}-form-modal--full` : undefined]} > <NSpin class={`${p}-form-modal__spin`} show={submiting}> <NScrollbar class={`${p}-form-modal__scrollbar`}> <div class={{ [`${p}-form-modal__form-wrap`]: true, [`${p}-form-modal__form-wrap--readonly`]: binds.readonly, }} style={contentStyle} > <DtForm {...binds} cols={24} onSubmit={onSubmit} ref={formRef} /> </div> </NScrollbar> </NSpin> </div> ), action: () => ( <NFlex justify="end"> {btns.map(({ text, tip, ...btn }) => { const node = ( <NButton key={text} {...btn}> {text} </NButton> ) return tip ? withDirectives(node, [[vTooltip, tip, '', {}]]) : node })} </NFlex> ), }} </NCard> ) }, renderTableBtns: ({ btns, model, index, width }) => { btns = btns.filter((btn) => !btn.hidden?.(model, index)) let more let _allw = btns.reduce((w, btn) => w + btn._width, 0) + 14 * (btns.length - 1) if (_allw > width) { let i = 0, w = 0 while (i < btns.length) { const _w = w + btns[i]._width if (_w + (i === btns.length - 1 ? 0 : 14 + moreWidth.value) > width) { break } w = _w + 14 i++ } more = btns.slice(i).map((btn) => { const text = btn?.text?.(model) return { ...btn, label: text, key: text, disabled: btn.disabled?.(model, index), } }) btns = btns.slice(0, i) } const renderLabel = (option) => { const title = option.title?.(model, index) const node = <NText type={option.type}>{option.label}</NText> if (!title) { return node } return <NTooltip placement="left">{{ trigger: () => node, default: () => title }}</NTooltip> } return ( <NFlex size={14} inline style={{ width: `${width}px` }}> {btns.map((btn) => { const text = btn?.text?.(model, index) return renderTableBtn({ ...btn, model, index, title: btn?.title?.(model, index), text, disabled: btn?.disabled?.(model, index), onClick: (cmd) => btn.onClick?.(model, index, cmd), }) })} {more ? ( <NDropdown options={more} trigger="hover" show-arrow renderLabel={renderLabel} onSelect={(_, btn) => btn.onClick?.(model, index)} > <NButton text type="primary"> {i18n.t('table.op.more')} </NButton> </NDropdown> ) : undefined} </NFlex> ) }, renderTag: ({ text, model: _0, ...props }) => <NTag {...props}>{text}</NTag>, renderState: ({ type, text }) => ( <> <NText type={type} style={{ marginRight: '5px', fontSize: '13px' }}></NText> <span>{text}</span> </> ), renderFiles(ids) { return <NFiles type="file" files={ids} /> }, renderImages(ids) { return <NFiles type="image" files={ids} /> }, renderUsers(users, options = {}) { options.size ||= 24 options.max ||= 4 const { max, size } = options if (users.length <= 1) { return <span style="position: relative">{users.map((u) => renderUser(u, true, options))}</span> } return ( <span style={{ display: 'inline-block', height: size + 'px' }}> <NAvatarGroup options={users} size={size} max={max || 4} style="top: -2px"> {{ avatar: ({ option }) => { return renderUser(option, false, options) }, rest: ({ options, rest }) => { return ( <NDropdown options={options} keyField="username" labelField="nickname" renderOption={({ option }) => renderUser(option, false, options)} > <NAvatar>+{rest}</NAvatar> </NDropdown> ) }, }} </NAvatarGroup> </span> ) }, renderDepts(depts) { return depts.map((dept) => <NTag key={dept.code}>{dept.name}</NTag>) }, dataFormatters: {}, formPresets: import('./form').then((m) => m.default), }, } } function handleFeedbackApi(target, source, types, methods, typeParamIndex = 0) { for (const t of types) { target[t] = (...params) => { if (!params[typeParamIndex]) { params[typeParamIndex] = {} } params[typeParamIndex].type = t return target(...params) } } for (const m of methods) { target[m] = (...params) => source()[m](...params) } return target } let style function createStyle(cls) { if (!style) { style = cB( 'form-modal', { maxHeight: 'calc(80vh - 100px)', ...flexDirCol, overflow: 'hidden', }, [ cM('full', { maxHeight: '100%', }), cE( 'spin', { overflow: 'hidden', ...flexDirCol, }, [ c('.n-spin-content', { overflow: 'hidden', ...flexDirCol, }), ], ), cE('scrollbar', { ...flexDirCol, }), cE( 'form-wrap', { padding: '20px 40px 0 0', }, [cM('readonly', { padding: '16px 24px' })], ), ], ) style.mount({ id: cls, anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME, }) } }