@ithinkdt/naive
Version:
iThinkDT Naive UI
612 lines (575 loc) • 22.5 kB
JSX
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,
})
}
}