init
@@ -0,0 +1 @@
|
||||
无样式的基础组件
|
||||
@@ -0,0 +1 @@
|
||||
export { default as NTooltip } from './tooltip'
|
||||
@@ -0,0 +1,173 @@
|
||||
import { syncRefs, useVModel } from '@vueuse/core'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import {
|
||||
Teleport,
|
||||
Transition,
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
reactive,
|
||||
ref,
|
||||
renderSlot,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { provideShow } from '../hooks/show'
|
||||
|
||||
export const tooltipProps = {
|
||||
popClass: {
|
||||
type: String,
|
||||
},
|
||||
position: {
|
||||
type: String as PropType<'bottom' | 'top' | 'left' | 'right' | 'auto'>,
|
||||
default: () => 'auto',
|
||||
},
|
||||
offset: {
|
||||
type: Number,
|
||||
default: () => 8,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ITooltip',
|
||||
props: tooltipProps,
|
||||
emits: ['change', 'update:visible'],
|
||||
setup(props, { slots, emit }) {
|
||||
const isOpen = ref(props.visible)
|
||||
|
||||
syncRefs(useVModel(props, 'visible'), isOpen)
|
||||
|
||||
provideShow(isOpen)
|
||||
|
||||
let timer: NodeJS.Timeout
|
||||
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
const popupRef = ref<HTMLElement>()
|
||||
|
||||
const dropdownPosition = reactive({ x: 0, y: 0 })
|
||||
|
||||
const isShow = computed(
|
||||
() =>
|
||||
isOpen.value &&
|
||||
triggerRef.value &&
|
||||
popupRef.value &&
|
||||
dropdownPosition.x + dropdownPosition.y > 0,
|
||||
)
|
||||
|
||||
const dropdownStyle = computed<CSSProperties>(() => {
|
||||
return {
|
||||
left: `${dropdownPosition.x}px`,
|
||||
top: `${dropdownPosition.y}px`,
|
||||
visibility: isShow.value ? 'visible' : 'hidden',
|
||||
}
|
||||
})
|
||||
|
||||
const mounted = ref(false)
|
||||
|
||||
function onMouseover() {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
timer = setTimeout(
|
||||
() => {
|
||||
isOpen.value = true
|
||||
emit('change', true)
|
||||
},
|
||||
props.lazy ? 800 : 100,
|
||||
)
|
||||
}
|
||||
|
||||
function onMouseout() {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
clearTimeout(timer)
|
||||
isOpen.value = false
|
||||
emit('change', false)
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const trigger = triggerRef.value
|
||||
const popup = popupRef.value
|
||||
|
||||
if (!!trigger && !!popup) {
|
||||
const { width, height, left, top } = trigger.getBoundingClientRect()
|
||||
|
||||
const { clientWidth: pWidth, clientHeight: pHeight } = popup
|
||||
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
const offset = props.offset
|
||||
|
||||
switch (props.position) {
|
||||
case 'auto':
|
||||
case 'bottom':
|
||||
x = left + width / 2
|
||||
y = top + height + offset
|
||||
break
|
||||
case 'top':
|
||||
x = left
|
||||
y = top - pHeight - offset
|
||||
break
|
||||
case 'left':
|
||||
x = left - pWidth - offset
|
||||
y = top
|
||||
break
|
||||
case 'right':
|
||||
x = left + width + offset
|
||||
y = top
|
||||
break
|
||||
}
|
||||
if (props.position == 'auto') {
|
||||
x = window.innerWidth - x > 500 ? x + 20 : x - 20 - 310
|
||||
y =
|
||||
window.innerHeight - y > popup.offsetHeight + 50
|
||||
? y
|
||||
: window.innerHeight - (popup.offsetHeight + 50)
|
||||
}
|
||||
dropdownPosition.x = x
|
||||
dropdownPosition.y = y
|
||||
}
|
||||
}
|
||||
|
||||
watch(isOpen, (val) => {
|
||||
if (val) {
|
||||
mounted.value = true
|
||||
}
|
||||
nextTick(resize)
|
||||
})
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div {...{ onMouseout, onMouseover }} ref={triggerRef}>
|
||||
{renderSlot(slots, 'default')}
|
||||
<Teleport to="body">
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
{mounted.value && !props.disabled && (
|
||||
<div
|
||||
class={['i-popper z-999', props.popClass]}
|
||||
style={dropdownStyle.value}
|
||||
ref={popupRef}
|
||||
>
|
||||
{renderSlot(slots, 'popper')}
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
DNF风格定制组件
|
||||
@@ -0,0 +1,207 @@
|
||||
<script lang="tsx">
|
||||
import { onKeyStroke, syncRef, useVModel } from '@vueuse/core'
|
||||
import type { PropType } from 'vue'
|
||||
import { defineComponent, ref, renderSlot } from 'vue'
|
||||
|
||||
import CalcDialog from '@/components/calc/dialog/index.vue'
|
||||
import CalcButton from '@/components/calc/button/index.vue'
|
||||
import type { JSX } from 'vue/jsx-runtime'
|
||||
|
||||
export type ActionDialogResult = 'ok' | 'reject' | 'cancel' | 'none'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IActionDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
// 模态框
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
header: {
|
||||
type: [String, Boolean] as PropType<string | boolean>,
|
||||
default: () => true,
|
||||
},
|
||||
drag: {
|
||||
type: String as PropType<'header' | 'all' | 'none'>,
|
||||
default: () => 'header',
|
||||
},
|
||||
okButton: {
|
||||
type: [String, Boolean],
|
||||
default: '确定',
|
||||
},
|
||||
rejectButton: {
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
},
|
||||
cancelButton: {
|
||||
type: [String, Boolean],
|
||||
default: '取消',
|
||||
},
|
||||
closeOnYes: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
closeOnCancel: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
closeOnReject: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
closeOnEsc: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
cache: {
|
||||
// 维持窗口状态
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
defaultStatus: {
|
||||
type: String as PropType<ActionDialogResult>,
|
||||
default: () => 'cancel',
|
||||
},
|
||||
appendTo: {
|
||||
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
|
||||
default: 'body',
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1.0,
|
||||
},
|
||||
},
|
||||
emits: ['close', 'ok', 'cancel', 'action', 'update:visible'],
|
||||
setup(props, { emit, slots }) {
|
||||
const visible = ref(props.visible)
|
||||
|
||||
syncRef(useVModel(props, 'visible'), visible, { direction: 'both' })
|
||||
|
||||
const result = ref<ActionDialogResult>(props.defaultStatus)
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
emit('close', result.value)
|
||||
}
|
||||
|
||||
onKeyStroke('ESC', (e: Event) => {
|
||||
if (visible.value && props.closeOnEsc) {
|
||||
e.preventDefault()
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
function onOkClick() {
|
||||
result.value = 'ok'
|
||||
if (props.closeOnYes) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
function onRejectClick() {
|
||||
result.value = 'reject'
|
||||
if (props.closeOnReject) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
function onCancelClick() {
|
||||
result.value = 'cancel'
|
||||
if (props.closeOnCancel) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
function onCloseClick() {
|
||||
close()
|
||||
}
|
||||
|
||||
function renderAction() {
|
||||
if ((props.okButton || props.cancelButton || props.rejectButton) && !slots.action) {
|
||||
const buttons: JSX.Element[] = []
|
||||
if (props.okButton) {
|
||||
buttons.push(
|
||||
<CalcButton class="mx-2px" type="primary" onClick={onOkClick}>
|
||||
{props.okButton === true ? '确定' : props.okButton}
|
||||
</CalcButton>,
|
||||
)
|
||||
}
|
||||
if (props.rejectButton) {
|
||||
buttons.push(
|
||||
<CalcButton class="mx-2px" type="primary" onClick={onRejectClick}>
|
||||
{props.rejectButton === true ? '拒绝' : props.rejectButton}
|
||||
</CalcButton>,
|
||||
)
|
||||
}
|
||||
if (props.cancelButton) {
|
||||
buttons.push(
|
||||
<CalcButton class="mx-2px" onClick={onCancelClick}>
|
||||
{props.cancelButton === true ? '取消' : props.cancelButton}
|
||||
</CalcButton>,
|
||||
)
|
||||
}
|
||||
|
||||
return <div class="flex pb-2 items-center justify-center">{buttons}</div>
|
||||
}
|
||||
return renderSlot(slots, 'action')
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<CalcDialog
|
||||
onClose={onCloseClick}
|
||||
v-model:visible={visible.value}
|
||||
drag={props.drag}
|
||||
mask={props.mask}
|
||||
modal={props.modal}
|
||||
header={props.header == true ? '提示' : props.header}
|
||||
appendTo={props.appendTo}
|
||||
scale={props.scale}
|
||||
>
|
||||
<div class={['flex items-center justify-center']}> {renderSlot(slots, 'default')}</div>
|
||||
{renderAction()}
|
||||
</CalcDialog>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.i-button {
|
||||
text-shadow: none;
|
||||
|
||||
color: #a89679;
|
||||
background: linear-gradient(#12396b, #081f3c);
|
||||
border: 1px solid #554a3d;
|
||||
|
||||
&:hover {
|
||||
color: #fee97d;
|
||||
background: linear-gradient(#2d4e9f, #153051);
|
||||
// border: 1px solid #e3c18a;
|
||||
color: #d4d4b6;
|
||||
background: linear-gradient(#195cab, #0b3260);
|
||||
border: 1px solid #867760;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #e9c556;
|
||||
background: linear-gradient(#2d4e9f, #122438);
|
||||
border: 1px solid #e3c18a;
|
||||
color: #d4d4b6;
|
||||
background: linear-gradient(#195cab, #0b3260);
|
||||
border: 1px solid #918168;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<script lang="tsx">
|
||||
import { debouncedWatch, onClickOutside, useVModel } from '@vueuse/core'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import {
|
||||
Teleport,
|
||||
Transition,
|
||||
computed,
|
||||
defineComponent,
|
||||
onDeactivated,
|
||||
ref,
|
||||
renderList,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { getElementZoom } from '@/utils/position'
|
||||
|
||||
type HandleChangeFunction = (value: string | number) => string[] | Promise<string[]>
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: () => '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
handleChange: {
|
||||
type: Function as PropType<HandleChangeFunction>,
|
||||
default: () => () => [],
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const modelValue = useVModel(props, 'modelValue')
|
||||
|
||||
const isOpen = ref(false)
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
|
||||
watch(isOpen, onResize)
|
||||
|
||||
// 下拉框位置
|
||||
const dropdownPosition = ref({ x: 0, y: 0, w: 0 })
|
||||
// 下拉框位置
|
||||
const dropdownStyle = computed<CSSProperties>(() => {
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
top: `${dropdownPosition.value.y}px`,
|
||||
width: `${dropdownPosition.value.w}px`,
|
||||
}
|
||||
})
|
||||
|
||||
function onResize() {
|
||||
if (triggerRef.value) {
|
||||
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
|
||||
const zoom = getElementZoom('body')
|
||||
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: left / zoom,
|
||||
y: (top + height + 2) / zoom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(triggerRef, () => (isOpen.value = false))
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('scroll', onResize)
|
||||
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('scroll', onResize)
|
||||
})
|
||||
|
||||
function selectItem(item: string) {
|
||||
return () => {
|
||||
modelValue.value = item
|
||||
}
|
||||
}
|
||||
|
||||
const delay = computed(() => props.delay)
|
||||
|
||||
const droplist = ref<string[]>([])
|
||||
|
||||
debouncedWatch(
|
||||
modelValue,
|
||||
async (value) => (droplist.value = await props.handleChange(value)),
|
||||
{
|
||||
debounce: delay,
|
||||
},
|
||||
)
|
||||
|
||||
function handleInput() {
|
||||
isOpen.value = droplist.value.length > 0
|
||||
}
|
||||
|
||||
function handleKeyPress(event: KeyboardEvent) {
|
||||
if (event.code == 'Enter') {
|
||||
emit('enter', modelValue.value)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="i-autocomplete">
|
||||
<div class="i-autocomplete-trigger" ref={triggerRef}>
|
||||
<input
|
||||
onKeypress={handleKeyPress}
|
||||
class="i-autocomplete-input"
|
||||
onFocus={handleInput}
|
||||
onInput={handleInput}
|
||||
type="text"
|
||||
v-model={modelValue.value}
|
||||
placeholder={props.placeholder}
|
||||
/>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
<div
|
||||
class="i-autocomplete-dropdown"
|
||||
style={dropdownStyle.value}
|
||||
v-show={isOpen.value}
|
||||
>
|
||||
{renderList(droplist.value, (item) => (
|
||||
<div class="i-autocomplete-dropdown-item" onClick={selectItem(item)}>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$text-color: #937639;
|
||||
.i-autocomplete {
|
||||
min-width: 80px;
|
||||
width: 160px;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
height: 24px;
|
||||
min-height: 16px;
|
||||
line-height: 100%;
|
||||
// font-size: 12px;
|
||||
color: $text-color;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
border: 1px solid #5b472a;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
|
||||
.i-autocomplete-trigger {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.i-autocomplete-input {
|
||||
padding-left: 8px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
// font-size: 12px;
|
||||
background-color: black;
|
||||
font-family: SimSun;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: gray;
|
||||
|
||||
.i-select-down-icon {
|
||||
background-image: url('./img/select_down_disabled.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.i-autocomplete-trigger:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.i-autocomplete-dropdown {
|
||||
position: fixed;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
background: black;
|
||||
// font-size: 12px;
|
||||
z-index: 888;
|
||||
|
||||
$hoverColor: #002947;
|
||||
$activeColor: color.scale($hoverColor, $lightness: 5%);
|
||||
color: $text-color;
|
||||
|
||||
.i-autocomplete-dropdown-item {
|
||||
// font-size: 12px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
background-color: $activeColor;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
// font-size: 12px;
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, renderSlot } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CalcButton',
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
class: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { slots, emit }) {
|
||||
return () => {
|
||||
const proerpties = {
|
||||
to: props.to,
|
||||
class: {
|
||||
[props.class]: !!props.class,
|
||||
'i-button cursor-pointer outline-none text-shadow': true,
|
||||
small: props.small,
|
||||
[`icon-${props.icon}`]: !!props.icon,
|
||||
},
|
||||
onClick: (e: MouseEvent) => {
|
||||
if (props.to) {
|
||||
e.preventDefault()
|
||||
}
|
||||
emit('click', e)
|
||||
},
|
||||
disabled: props.disabled,
|
||||
}
|
||||
const content = renderSlot(slots, 'default')
|
||||
return props.to ? (
|
||||
<RouterLink {...proerpties}>{content}</RouterLink>
|
||||
) : (
|
||||
<button {...proerpties}>{content}</button>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-button {
|
||||
min-width: 56px;
|
||||
height: 24px;
|
||||
// line-height: 20px;
|
||||
|
||||
// font-size: 12px;
|
||||
color: #e9c556;
|
||||
background: linear-gradient(#223768, #122438);
|
||||
border: 1px solid #755f44;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: all 200ms ease-in-out;
|
||||
|
||||
&[class*='icon-'] {
|
||||
min-width: 1px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fee97d;
|
||||
background: linear-gradient(#2d4e9f, #153051);
|
||||
// border: 1px solid #e3c18a;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #e9c556;
|
||||
background: linear-gradient(#2d4e9f, #122438);
|
||||
border: 1px solid #e3c18a;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #696969;
|
||||
background: linear-gradient(#353535, #1c1c1c);
|
||||
border: 1px solid #414141;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
min-width: 28px;
|
||||
height: 20px;
|
||||
// line-height: 20px;
|
||||
font-size: 12px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.i-href {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 205 B |
|
After Width: | Height: | Size: 205 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 215 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1,277 @@
|
||||
<script lang="tsx">
|
||||
import { selectionEmits, selectionProps } from 'hoci'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import { Teleport, Transition, computed, defineComponent, onDeactivated, ref, watch } from 'vue'
|
||||
|
||||
import { onClickOutside, useVModel } from '@vueuse/core'
|
||||
import { getElementZoom } from '@/utils/position'
|
||||
interface ICascaderItem {
|
||||
label: string
|
||||
id?: string | number
|
||||
value?: any
|
||||
children?: ICascaderItem[]
|
||||
onSelect?: () => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ICascader',
|
||||
props: {
|
||||
...selectionProps,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
emptyLabel: {
|
||||
type: String,
|
||||
},
|
||||
highlight: {
|
||||
type: String,
|
||||
// warn remind
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<ICascaderItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: [...selectionEmits, 'select'],
|
||||
setup(props, { emit }) {
|
||||
const modelValue = useVModel(props, 'modelValue')
|
||||
|
||||
const isOpen = ref(false)
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
const selectRef = ref<HTMLElement>()
|
||||
const dropdownRef = ref<HTMLElement>()
|
||||
|
||||
watch(isOpen, onResize)
|
||||
|
||||
function collapse() {
|
||||
isOpen.value = !isOpen.value && !props.disabled
|
||||
}
|
||||
|
||||
// 下拉框位置
|
||||
const dropdownPosition = ref({ x: 0, y: 0, w: 0, z: 0 })
|
||||
// 下拉框位置
|
||||
const dropdownStyle = computed<CSSProperties>(() => {
|
||||
if (dropdownPosition.value.z == 0) {
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
top: `${dropdownPosition.value.y}px`,
|
||||
width: `${dropdownPosition.value.w}px`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
bottom: `${dropdownPosition.value.z}px`,
|
||||
width: `${dropdownPosition.value.w}px`,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onResize() {
|
||||
if (triggerRef.value) {
|
||||
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
|
||||
const zoom = getElementZoom('body')
|
||||
if (window.innerHeight - top > 160) {
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: left / zoom,
|
||||
y: (top + height + 2) / zoom,
|
||||
z: 0,
|
||||
}
|
||||
} else {
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: left / zoom,
|
||||
z: (window.innerHeight - top + 2) / zoom,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('scroll', onResize)
|
||||
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('scroll', onResize)
|
||||
})
|
||||
|
||||
const selectItem = ref<ICascaderItem>()
|
||||
|
||||
function onSelect(item: ICascaderItem) {
|
||||
if (!item.children?.length) {
|
||||
isOpen.value = false
|
||||
}
|
||||
selectItem.value = item
|
||||
emit('select', item.value)
|
||||
}
|
||||
|
||||
function loadNode(item: ICascaderItem) {
|
||||
const value = props.modelValue ?? props.defaultValue
|
||||
if ((selectItem.value == null || selectItem.value == undefined) && value == item.value) {
|
||||
selectItem.value = item
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(triggerRef, () => (isOpen.value = false), { ignore: [dropdownRef] })
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div
|
||||
class={['min-w-20 w-40 i-cascader'].concat(props.highlight)}
|
||||
onClick={collapse}
|
||||
ref={selectRef}
|
||||
>
|
||||
<div
|
||||
class={{
|
||||
'i-cascader-trigger': true,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
ref={triggerRef}
|
||||
>
|
||||
<span class="i-cascader-label">{selectItem.value?.label}</span>
|
||||
<calc-button icon="dropdown"></calc-button>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
<calc-tree
|
||||
{...props}
|
||||
v-model={modelValue.value}
|
||||
class="py-1 i-cascader-dropdown"
|
||||
style={dropdownStyle.value}
|
||||
v-show={isOpen.value}
|
||||
ref={dropdownRef}
|
||||
onLoad={loadNode}
|
||||
onSelect={onSelect}
|
||||
data={props.items}
|
||||
/>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:color';
|
||||
$text-color: #937639;
|
||||
.i-cascader {
|
||||
min-width: 80px;
|
||||
width: 160px;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
line-height: 100%;
|
||||
// font-size: 12px;
|
||||
color: $text-color;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
border: 1px solid #5b472a;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
padding-right: 3px;
|
||||
margin: 0;
|
||||
display: block;
|
||||
|
||||
.i-cascader-trigger {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
opacity: 0.9;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.i-cascader-label {
|
||||
padding-left: 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.i-cascader-down-icon {
|
||||
background-image: url('./img/select_down.png');
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 2px;
|
||||
width: 15px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.i-cascader-down-icon:active {
|
||||
background-image: url('./img/select_down_clicked.png');
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 2px;
|
||||
width: 15px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: gray;
|
||||
|
||||
.i-cascader-down-icon {
|
||||
background-image: url('./img/select_down_disabled.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.i-cascader-trigger:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.remind {
|
||||
color: #32e432 !important;
|
||||
border: 1px solid #aa8651;
|
||||
}
|
||||
|
||||
.i-cascader-dropdown {
|
||||
position: fixed;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: black;
|
||||
// font-size: 12px;
|
||||
z-index: 888;
|
||||
|
||||
$hoverColor: #002947;
|
||||
$activeColor: color.scale($hoverColor, $lightness: 5%);
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, provide } from 'vue'
|
||||
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
|
||||
import { CHECKBOX_GROUP } from '../shared'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ICheckboxGroup',
|
||||
components: {},
|
||||
props: {
|
||||
...selectionProps,
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'checked',
|
||||
},
|
||||
unactiveClass: {
|
||||
type: String,
|
||||
default: 'unchecked',
|
||||
},
|
||||
multiple: {
|
||||
type: [Boolean, Number],
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: [...selectionEmits],
|
||||
|
||||
setup(props, context) {
|
||||
provide(CHECKBOX_GROUP, true)
|
||||
|
||||
const { render } = useSelectionList(props, context)
|
||||
|
||||
return () => render()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.checked {
|
||||
.i-checkbox-icon {
|
||||
background-image: url('@/assets/img/control/checkbox_checked.png');
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-icon {
|
||||
width: 12px;
|
||||
min-width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
background-image: url('@/assets/img/control/checkbox_uncheck.png');
|
||||
}
|
||||
|
||||
.i-checkbox-icon-disable {
|
||||
width: 12px;
|
||||
min-width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
background-image: url('@/assets/img/control/checkbox_disable.png');
|
||||
}
|
||||
|
||||
&:hover:not(.checked) {
|
||||
.i-checkbox-icon {
|
||||
background-image: url('@/assets/img/control/checkbox_hover.png');
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-label {
|
||||
line-height: 24px;
|
||||
color: #937639;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<script lang="tsx">
|
||||
import type { EmitsOptions, SetupContext } from 'vue'
|
||||
import { computed, defineComponent, inject, nextTick, renderSlot } from 'vue'
|
||||
import { selectionProps, switchEmits, switchProps, useSelectionItem, useSwitch } from 'hoci'
|
||||
import { CHECKBOX_GROUP } from '../shared'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ICheckbox',
|
||||
components: {},
|
||||
props: {
|
||||
...switchProps,
|
||||
...selectionProps,
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
},
|
||||
emits: [...switchEmits],
|
||||
|
||||
setup(props, context) {
|
||||
const inGroup = computed(() => {
|
||||
return inject(CHECKBOX_GROUP, false)
|
||||
})
|
||||
|
||||
const { toggle, className: switchClass } = useSwitch(
|
||||
() => ({
|
||||
...props,
|
||||
class: props.round
|
||||
? 'i-switch text-xs h-auto cursor-pointer'
|
||||
: 'i-checkbox text-xs h-auto cursor-pointer',
|
||||
activeClass: 'checked',
|
||||
}),
|
||||
context,
|
||||
)
|
||||
|
||||
const { activate, className: itemClass } = useSelectionItem(
|
||||
props,
|
||||
context as SetupContext<EmitsOptions>,
|
||||
)
|
||||
|
||||
const baseClass = computed(() => (props.round ? 'i-switch' : 'i-checkbox'))
|
||||
|
||||
const { slots } = context
|
||||
|
||||
async function onClick() {
|
||||
if (inGroup.value) {
|
||||
activate()
|
||||
} else {
|
||||
toggle()
|
||||
}
|
||||
await nextTick()
|
||||
// console.log(props.modelValue)
|
||||
}
|
||||
|
||||
const className = computed(() => {
|
||||
return inGroup.value ? itemClass.value : switchClass.value
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div onClick={onClick} class={['flex', baseClass.value, className.value]}>
|
||||
<span class={!props.disabled ? 'i-checkbox-icon' : 'i-checkbox-icon-disable'}></span>
|
||||
<div class={['i-checkbox-label h-auto']}>{props.label ?? renderSlot(slots, 'default')}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: baseline;
|
||||
&.checked {
|
||||
.i-checkbox-icon {
|
||||
background-image: url('@/assets/img/control/checkbox_checked.png');
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-icon {
|
||||
width: 12px;
|
||||
min-width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
background-image: url('@/assets/img/control/checkbox_uncheck.png');
|
||||
}
|
||||
|
||||
.i-checkbox-icon-disable {
|
||||
width: 12px;
|
||||
min-width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
background-image: url('@/assets/img/control/checkbox_disable.png');
|
||||
}
|
||||
|
||||
&:hover:not(.checked) {
|
||||
.i-checkbox-icon {
|
||||
background-image: url('@/assets/img/control/checkbox_hover.png');
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-label {
|
||||
line-height: 24px;
|
||||
color: #937639;
|
||||
}
|
||||
}
|
||||
|
||||
.i-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
&.checked {
|
||||
.i-checkbox-icon {
|
||||
background-image:
|
||||
linear-gradient(to right, #8f6e29, #8f6e29),
|
||||
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
|
||||
}
|
||||
|
||||
.i-checkbox-icon::after {
|
||||
left: 100%;
|
||||
margin-left: -14.8px;
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-icon {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
|
||||
outline: none;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #dcdfe6;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.3s,
|
||||
background-color 0.3s;
|
||||
vertical-align: middle;
|
||||
// border-radius: 16px;
|
||||
border: 1.5px solid transparent;
|
||||
background-clip: padding-box, border-box;
|
||||
background-origin: padding-box, border-box;
|
||||
background-image:
|
||||
linear-gradient(to right, #141414, #141414),
|
||||
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
|
||||
box-shadow: inset 0 0 2px 1.5px black;
|
||||
}
|
||||
|
||||
.i-checkbox-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1.6px;
|
||||
left: 1px;
|
||||
border-radius: 100%;
|
||||
transition: all 0.3s;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
outline: 0.8px solid black;
|
||||
background: linear-gradient(rgb(15, 61, 116), rgb(8, 37, 71));
|
||||
box-shadow: inset 0 0 2px 0.5px rgb(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.i-checkbox-icon-disable {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
|
||||
outline: none;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #dcdfe6;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.3s,
|
||||
background-color 0.3s;
|
||||
vertical-align: middle;
|
||||
// border-radius: 16px;
|
||||
border: 1.5px solid transparent;
|
||||
background-clip: padding-box, border-box;
|
||||
background-origin: padding-box, border-box;
|
||||
background-image:
|
||||
linear-gradient(to right, #dcdfe6, #dcdfe6),
|
||||
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
|
||||
box-shadow: inset 0 0 2px 1.5px black;
|
||||
}
|
||||
|
||||
.i-checkbox-icon-disable::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1.6px;
|
||||
left: 1px;
|
||||
border-radius: 100%;
|
||||
transition: all 0.3s;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
// outline: 0.8px solid black;
|
||||
background: white;
|
||||
// box-shadow: inset 0 0 2px 0.5px rgb(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.i-checkbox-icon::after {
|
||||
background: linear-gradient(#15549f, #0e3c72);
|
||||
}
|
||||
}
|
||||
|
||||
.i-checkbox-label {
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
padding-left: 2px;
|
||||
color: #937639;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="tsx">
|
||||
import { useToggle, useVModel } from '@vueuse/core'
|
||||
import { defineComponent, renderSlot } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ICollapse',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
},
|
||||
setup(props, { slots, emit }) {
|
||||
const modelValue = useVModel(props, 'modelValue', emit, { passive: true })
|
||||
|
||||
const toggle = useToggle(modelValue)
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="rounded h-auto bg-hex-00000078 border-1 border-hex-ffffff28 my-1 overflow-hidden">
|
||||
<div
|
||||
onClick={() => toggle()}
|
||||
v-text={props.title}
|
||||
class="bg-gradient-to-b flex from-hex-244281 to-hex-122438 h-6 text-xs text-center w-full items-center justify-center"
|
||||
></div>
|
||||
<div
|
||||
class={['transition-all', 'ease-out-in', 'h-auto'].concat(
|
||||
modelValue.value ? ['max-h-200'] : ['max-h-0'],
|
||||
)}
|
||||
>
|
||||
<div class="p-2">{renderSlot(slots, 'default')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script lang="tsx">
|
||||
import { onClickOutside, onKeyDown, syncRef, useDraggable, useVModel } from '@vueuse/core'
|
||||
import type { PropType } from 'vue'
|
||||
import {
|
||||
Teleport,
|
||||
Transition,
|
||||
computed,
|
||||
defineComponent,
|
||||
provide,
|
||||
ref,
|
||||
renderSlot,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { provideShow } from '@/components/hooks/show'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
// 模态框
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
header: {
|
||||
type: [String, Boolean],
|
||||
default: () => '提示',
|
||||
},
|
||||
closeOnEsc: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
closeButton: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
cache: {
|
||||
// 维持窗口状态
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
drag: {
|
||||
type: String as PropType<'header' | 'all' | 'none'>,
|
||||
default: () => 'header',
|
||||
},
|
||||
appendTo: {
|
||||
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
|
||||
default: 'body',
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
emits: ['close', 'update:visible'],
|
||||
setup(props, { emit, slots }) {
|
||||
const dialogRef = ref<HTMLElement | null>(null)
|
||||
const headerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const visible = ref(props.visible)
|
||||
|
||||
provideShow(visible)
|
||||
|
||||
provide('dialogRef', dialogRef)
|
||||
|
||||
const loaded = ref(!props.lazy)
|
||||
|
||||
const stopLoadWatch = watch(visible, (val) => {
|
||||
if (val) {
|
||||
loaded.value = true
|
||||
stopLoadWatch()
|
||||
}
|
||||
})
|
||||
|
||||
syncRef(useVModel(props, 'visible'), visible, { direction: 'both' })
|
||||
|
||||
onClickOutside(dialogRef, () => {
|
||||
if (props.modal) {
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onKeyDown(
|
||||
'Escape',
|
||||
() => {
|
||||
if (visible.value && props.closeOnEsc) {
|
||||
close()
|
||||
}
|
||||
},
|
||||
{ target: document },
|
||||
)
|
||||
|
||||
const init = ref(true)
|
||||
|
||||
const { x, y, style } = useDraggable(
|
||||
computed(() => {
|
||||
switch (props.drag) {
|
||||
case 'header':
|
||||
return headerRef.value
|
||||
case 'all':
|
||||
return dialogRef.value
|
||||
case 'none':
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}),
|
||||
{
|
||||
initialValue: { x: 0, y: 0 },
|
||||
},
|
||||
)
|
||||
|
||||
const isFixed = computed(() => x.value + y.value > 0)
|
||||
|
||||
const appendToRef = computed(() => {
|
||||
return props.appendTo || 'body'
|
||||
})
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<Teleport to={appendToRef.value}>
|
||||
<Transition name="dialog" mode="out-in">
|
||||
{loaded.value && (props.cache || visible.value) && (
|
||||
<div
|
||||
v-show={visible.value}
|
||||
class={[
|
||||
'dialog-mask w-full h-full fixed top-0 left-0 z-999 flex justify-center items-center ',
|
||||
].concat(props.mask ? 'bg-hex-000 bg-opacity-66' : '')}
|
||||
style={`transform: scale(${props.scale});${x.value == 0 && y.value == 0 ? 'transform-origin:center' : 'transform-origin:left top'}`}
|
||||
>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
class={[
|
||||
'h-auto border-1 border-hex-2b2b2b border-solid shadow-sm round-1 dialog min-w-48',
|
||||
isFixed.value ? 'fixed' : '',
|
||||
props.class,
|
||||
]}
|
||||
style={`top:${y.value / props.scale}px;left:${x.value / props.scale}px`}
|
||||
>
|
||||
{props.header && (
|
||||
<div
|
||||
ref={headerRef}
|
||||
class="flex h-4 px-1 leading-4 z-9999 justify-center relative app-header py-2px"
|
||||
>
|
||||
<div class="text-xs text-shadow ">{props.header}</div>
|
||||
{props.closeButton && (
|
||||
<div class="flex top-0 right-0 bottom-0 items-center absolute">
|
||||
<div
|
||||
onClick={close}
|
||||
class="cursor-pointer flex h-4 text-center w-4 items-center close-icon"
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div class="bg-hex-000 bg-opacity-80 text-white text-xs">
|
||||
{renderSlot(slots, 'default')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
</Teleport>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-enter-active {
|
||||
animation: fade-in 400ms;
|
||||
.dialog {
|
||||
animation: zoom-in 400ms;
|
||||
}
|
||||
}
|
||||
.dialog-leave-active {
|
||||
animation: fade-in reverse 400ms;
|
||||
.dialog {
|
||||
animation: zoom-out 400ms;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<script lang="tsx">
|
||||
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import {
|
||||
Teleport,
|
||||
Transition,
|
||||
computed,
|
||||
defineComponent,
|
||||
onDeactivated,
|
||||
ref,
|
||||
renderSlot,
|
||||
watch,
|
||||
} from 'vue'
|
||||
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { getElementZoom } from '@/utils/position'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IIconselect',
|
||||
props: {
|
||||
...selectionProps,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 28,
|
||||
},
|
||||
emptyLabel: {
|
||||
type: String,
|
||||
},
|
||||
columnNum: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
appendTo: {
|
||||
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
|
||||
default: 'body',
|
||||
},
|
||||
},
|
||||
emits: selectionEmits,
|
||||
setup(props, context) {
|
||||
// props.itemClass = "i-select-dropdown-item"
|
||||
// const { active } = useSelectionList({ ...props, itemClass: "i-select-dropdown-item" }, context)
|
||||
const { renderItem } = useSelectionList(() => {
|
||||
return {
|
||||
...props,
|
||||
itemClass: 'i-option',
|
||||
activeClass: 'active',
|
||||
unactiveClass: 'unactive',
|
||||
}
|
||||
}, context)
|
||||
|
||||
// const { active } = useSelectionList(props, context)
|
||||
const columnNum = computed(() => props.columnNum || 1)
|
||||
// console.log({ ...props })
|
||||
|
||||
const isOpen = ref(false)
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
|
||||
watch(isOpen, onResize)
|
||||
|
||||
const appendToRef = computed(() => {
|
||||
return props.appendTo || 'body'
|
||||
})
|
||||
|
||||
function collapse() {
|
||||
isOpen.value = !isOpen.value && !props.disabled
|
||||
}
|
||||
|
||||
// 下拉框位置
|
||||
const dropdownPosition = ref({ x: 0, y: 0, w: 0 })
|
||||
// 下拉框位置
|
||||
const dropdownStyle = computed<CSSProperties>(() => {
|
||||
const zoom = getElementZoom(appendToRef.value)
|
||||
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
top: `${dropdownPosition.value.y}px`,
|
||||
width: `${(columnNum.value * 32 + 11) / zoom}px`,
|
||||
}
|
||||
})
|
||||
|
||||
function onResize() {
|
||||
if (triggerRef.value) {
|
||||
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
|
||||
const zoom = getElementZoom(appendToRef.value)
|
||||
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: (left - 2) / zoom,
|
||||
y: (top + height + 2) / zoom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(triggerRef, () => (isOpen.value = false))
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('scroll', onResize)
|
||||
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('scroll', onResize)
|
||||
})
|
||||
|
||||
const { slots } = context
|
||||
// console.log(active.value?.render())
|
||||
// console.log(active.value)
|
||||
// console.log(active.value?.render() ?? props.emptyLabel)
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="i-icon-select" onClick={collapse}>
|
||||
<div
|
||||
class={{
|
||||
'i-select-trigger': true,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
ref={triggerRef}
|
||||
>
|
||||
<span class="i-select-label">{renderItem() ?? props.emptyLabel}</span>
|
||||
</div>
|
||||
<Teleport to={appendToRef.value}>
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
<div class="i-icon-select-dropdown" style={dropdownStyle.value} v-show={isOpen.value}>
|
||||
{renderSlot(slots, 'default')}
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:color';
|
||||
$text-color: #e9c556;
|
||||
.i-icon-select {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
height: 28px;
|
||||
line-height: 100%;
|
||||
// font-size: 12px;
|
||||
color: $text-color;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
border: 1px solid #5b472a;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
|
||||
.i-select-trigger {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.disabled {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.i-select-trigger:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.i-icon-select-dropdown {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: fixed;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 480px;
|
||||
// background: rgba(255, 255, 255, 0.1);
|
||||
// font-size: 12px;
|
||||
z-index: 888;
|
||||
min-height: 28px;
|
||||
|
||||
$hoverColor: #002947;
|
||||
$activeColor: color.scale($hoverColor, $lightness: 5%);
|
||||
color: $text-color;
|
||||
|
||||
.i-option {
|
||||
// font-size: 12px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
margin: 1px;
|
||||
border: none;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: gray 1px solid;
|
||||
|
||||
// &.active {
|
||||
// background-color: $activeColor;
|
||||
// }
|
||||
|
||||
&.active::before {
|
||||
content: '';
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
background-image: url('/src/assets/img/control/item_checked.png');
|
||||
background-size: 80% 80%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
&:hover:not(.active) ::before {
|
||||
content: '';
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
background-image: url('@/assets/img/control/item_hover.png');
|
||||
background-size: 100% 100%;
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { App } from 'vue'
|
||||
import Button from './button/index.vue'
|
||||
import Tabs from './tabs/index.vue'
|
||||
import Tab from './tab/index.vue'
|
||||
import Select from './select/index.vue'
|
||||
import Option from './option/index.vue'
|
||||
import Menu from './menu/menu.vue'
|
||||
import Checkbox from './checkbox/index.vue'
|
||||
import Collapse from './collapse/index.vue'
|
||||
import Dialog from './dialog/index.vue'
|
||||
import Tooltip from './tooltip/index.vue'
|
||||
import Iconselect from './iconselect/index.vue'
|
||||
import Selection from './selection/index.vue'
|
||||
import Slider from './slider/index.vue'
|
||||
|
||||
export const components = {
|
||||
Button,
|
||||
Tabs,
|
||||
Option,
|
||||
Tab,
|
||||
Select,
|
||||
Menu,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
Dialog,
|
||||
Tooltip,
|
||||
Iconselect,
|
||||
Selection,
|
||||
Slider,
|
||||
}
|
||||
|
||||
export type CalcComponents = typeof components
|
||||
|
||||
export function install(app: App) {
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
app.component(`calc-${key.toLocaleLowerCase()}`, value)
|
||||
}
|
||||
}
|
||||
|
||||
export default components
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="tsx">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IInput',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const modelValue = useVModel(props, 'modelValue')
|
||||
return () => (
|
||||
<div class="flex min-w-80px h-20px border-1px box-border border-solid border-[#5b472a] rounded-[2px] bg-[#000000] px-2px">
|
||||
<input
|
||||
class="flex-1 border-none w-full outline-none h-auto text-[#ffe2ac] m-0 bg-[#000000]"
|
||||
type={props.type}
|
||||
v-model={modelValue.value}
|
||||
/>
|
||||
{props.type == 'number' && (
|
||||
<div class="h-full">
|
||||
<div
|
||||
class="input-number-upper h-50% w-10px"
|
||||
onClick={() => (modelValue.value = Number(modelValue.value) + 1)}
|
||||
></div>
|
||||
<div
|
||||
class="input-number-lower h-50% w-10px"
|
||||
onClick={() => (modelValue.value = Math.max(Number(modelValue.value) - 1, props.min))}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-number-upper {
|
||||
background-image: url('@/assets/img/icon/input_upper.png');
|
||||
background-size: 8px auto;
|
||||
background-position: bottom 1px center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.input-number-lower {
|
||||
background-image: url('@/assets/img/icon/input_lower.png');
|
||||
background-size: 8px auto;
|
||||
background-position: top 1px center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
<script lang="tsx">
|
||||
import { HiItem } from 'hoci'
|
||||
export default HiItem
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, renderSlot } from 'vue'
|
||||
import { classPropType } from '@/components/hooks/types'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
class: {
|
||||
type: classPropType,
|
||||
default: () => '',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const loading = computed(() => props.loading)
|
||||
return () => {
|
||||
return (
|
||||
<div class="relative">
|
||||
<div
|
||||
v-show={loading.value}
|
||||
class="flex h-full w-full z-99 absolute items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="i-loading-spin"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width="200px"
|
||||
height="200px"
|
||||
viewBox="0 0 200 200"
|
||||
>
|
||||
<g>
|
||||
<linearGradient
|
||||
id="right"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="150"
|
||||
y1="20"
|
||||
x2="150"
|
||||
y2="180"
|
||||
>
|
||||
<stop offset="0" style="stop-color:transparent" />
|
||||
<stop offset="1" style="stop-color:currentColor" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#right)"
|
||||
d="M100,0v20c44.1,0,80,35.9,80,80c0,44.1-35.9,80-80,80v20c55.2,0,100-44.8,100-100S155.2,0,100,0z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="left"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="50"
|
||||
y1="0"
|
||||
x2="50"
|
||||
y2="180"
|
||||
>
|
||||
<stop offset="0" style="stop-color:currentColor" />
|
||||
<stop offset="1" style="stop-color:transparent" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#left)"
|
||||
d="M20,100c0-44.1,35.9-80,80-80V0C44.8,0,0,44.8,0,100s44.8,100,100,100v-20C55.9,180,20,144.1,20,100z"
|
||||
/>
|
||||
<circle fill="currentColor" cx="100" cy="10" r="10" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class={[props.class].concat('duration-200').concat(loading.value ? 'opacity-0' : '')}
|
||||
>
|
||||
{renderSlot(slots, 'default', {}, () => [])}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-loading-spin {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: spin infinite 1s linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639282812376" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2304" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M85.312 85.312v853.376h853.376V85.312H85.312zM0 0h1024v1024H0V0z m554.624 213.312v256h256v85.376h-256v256H469.312v-256h-256V469.376h256v-256h85.312z" fill="currentColor" p-id="2305"></path></svg>
|
||||
|
After Width: | Height: | Size: 557 B |
@@ -0,0 +1,73 @@
|
||||
<script lang="tsx">
|
||||
import { useToggle } from '@vueuse/core'
|
||||
import { defineComponent, ref, renderSlot } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CalcMenu',
|
||||
props: ['label'],
|
||||
setup(props, { slots }) {
|
||||
const expand = ref(false)
|
||||
|
||||
const toggle = useToggle(expand)
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="i-menu">
|
||||
<div class="i-menu-content">
|
||||
<div onClick={() => toggle()} class="i-menu-expand-icon">
|
||||
<svg viewBox="0 0 1024 1024" width="100%" height="100%">
|
||||
<path
|
||||
d="M85.312 85.312v853.376h853.376V85.312H85.312zM0 0h1024v1024H0V0z m554.624 213.312v256h256v85.376h-256v256H469.312v-256h-256V469.376h256v-256h85.312z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="i-menu-label" v-trans={props.label}></div>
|
||||
</div>
|
||||
<div class={{ 'i-menu-items': true, expand: expand.value }}>
|
||||
{renderSlot(slots, 'default')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-menu {
|
||||
min-width: 120px;
|
||||
width: 200px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.i-menu-content {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
background-color: rgba(37, 37, 37, 0.4);
|
||||
padding: 4px;
|
||||
|
||||
.i-menu-expand-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: rgb(255, 247, 148);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.i-menu-label {
|
||||
width: auto;
|
||||
// font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.i-menu-items {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.expand {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
<script lang="tsx">
|
||||
import { HiItem } from 'hoci'
|
||||
export default HiItem
|
||||
</script>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="tsx">
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, defineComponent, onUnmounted } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
page: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
totalPage: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
minPage: {
|
||||
type: Number,
|
||||
default: () => 1,
|
||||
},
|
||||
parent: {
|
||||
type: Object as PropType<HTMLElement>,
|
||||
default: () => document,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const totalPage = computed(() =>
|
||||
Number.isNaN(props.totalPage) ? props.minPage : props.totalPage,
|
||||
)
|
||||
|
||||
const page = computed({
|
||||
get() {
|
||||
if (Number.isNaN(props.page)) {
|
||||
return props.minPage
|
||||
}
|
||||
return Math.max(Math.min(props.page, totalPage.value), props.minPage)
|
||||
},
|
||||
set(val) {
|
||||
emit('update:page', val)
|
||||
},
|
||||
})
|
||||
|
||||
function prev(e: Event) {
|
||||
e.preventDefault()
|
||||
if (page.value > 1 && !props.disabled) {
|
||||
page.value--
|
||||
emit('prev')
|
||||
emit('change', page.value)
|
||||
}
|
||||
}
|
||||
|
||||
function next(e: Event) {
|
||||
e.preventDefault()
|
||||
if (page.value < totalPage.value && !props.disabled) {
|
||||
page.value = page.value + 1
|
||||
emit('next')
|
||||
emit('change', page.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removePrev = onKeyStroke(['F1', 'PageUp'], prev, { target: props.parent })
|
||||
const removeNext = onKeyStroke(['F2', 'PageDown'], next, { target: props.parent })
|
||||
|
||||
onUnmounted(() => {
|
||||
removePrev()
|
||||
removeNext()
|
||||
})
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="flex space-x-1 h-10 w-full items-center justify-center">
|
||||
<calc-button
|
||||
icon="prev"
|
||||
disabled={page.value <= 1 || props.disabled}
|
||||
onClick={prev}
|
||||
></calc-button>
|
||||
<span class="text-center w-8">
|
||||
{page.value}
|
||||
/
|
||||
{totalPage.value}
|
||||
</span>
|
||||
<calc-button
|
||||
icon="next"
|
||||
disabled={page.value >= totalPage.value || props.disabled}
|
||||
onClick={next}
|
||||
></calc-button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
|
||||
total: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const progress = computed(() => {
|
||||
if (props.value == 0) {
|
||||
return 0
|
||||
}
|
||||
return (Number(props.value) / Number(props.total)) * 100
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="border-solid bg-hex-171717 border-hex-272521 border-2">
|
||||
<div class="border-transparent border-solid border-2">
|
||||
<div
|
||||
style={progress.value == 0 ? 'width:0' : `width:${progress.value.toFixed(2)}%`}
|
||||
class="h-2 duration-200 calc-progress"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.calc-progress {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
#48d0eb 0%,
|
||||
#6cf4fe 15%,
|
||||
#4fbdfe 30%,
|
||||
#3080fe 40%,
|
||||
#1d5bfe 55%,
|
||||
#3283fd 70%,
|
||||
#000456 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 205 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1,330 @@
|
||||
<script lang="tsx">
|
||||
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import {
|
||||
Teleport,
|
||||
Transition,
|
||||
computed,
|
||||
defineComponent,
|
||||
onDeactivated,
|
||||
ref,
|
||||
renderList,
|
||||
renderSlot,
|
||||
watch,
|
||||
} from 'vue'
|
||||
|
||||
import { onClickOutside, useVModel } from '@vueuse/core'
|
||||
import { classPropType, labelPropType } from '@/components/hooks/types'
|
||||
import { getElementZoom } from '@/utils/position'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ISelect',
|
||||
props: {
|
||||
...selectionProps,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
emptyLabel: {
|
||||
type: labelPropType,
|
||||
},
|
||||
highlight: {
|
||||
type: String,
|
||||
// warn remind
|
||||
default: '',
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputClass: {
|
||||
type: classPropType,
|
||||
default: '',
|
||||
},
|
||||
appendTo: {
|
||||
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
|
||||
default() {
|
||||
return 'body'
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: [...selectionEmits],
|
||||
setup(props, context) {
|
||||
const modelValue = useVModel(props, 'modelValue', context.emit, { passive: true })
|
||||
|
||||
const { renderItem } = useSelectionList(() => {
|
||||
return {
|
||||
...props,
|
||||
modelValue: modelValue.value,
|
||||
itemClass: 'i-select-dropdown-item',
|
||||
activeClass: 'active',
|
||||
unactiveClass: 'unactive',
|
||||
}
|
||||
}, context)
|
||||
|
||||
const isOpen = ref(false)
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
const selectRef = ref<HTMLElement>()
|
||||
const dropdownRef = ref<HTMLElement>()
|
||||
|
||||
watch(isOpen, onResize)
|
||||
|
||||
function collapse() {
|
||||
isOpen.value = !isOpen.value && !props.disabled
|
||||
}
|
||||
|
||||
// 下拉框位置
|
||||
const dropdownPosition = ref({ x: 0, y: 0, w: 0, z: 0 })
|
||||
// 下拉框位置
|
||||
const dropdownStyle = computed<CSSProperties>(() => {
|
||||
if (dropdownPosition.value.z == 0) {
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
top: `${dropdownPosition.value.y}px`,
|
||||
width: `${dropdownPosition.value.w}px`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
left: `${dropdownPosition.value.x}px`,
|
||||
bottom: `${dropdownPosition.value.z}px`,
|
||||
width: `${dropdownPosition.value.w}px`,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onResize() {
|
||||
if (triggerRef.value) {
|
||||
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
|
||||
const zoom = getElementZoom(appendToRef.value)
|
||||
if (window.innerHeight - top > 160) {
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: left / zoom,
|
||||
y: (top + height + 2) / zoom,
|
||||
z: 0,
|
||||
}
|
||||
} else {
|
||||
dropdownPosition.value = {
|
||||
w: width / zoom,
|
||||
x: left / zoom,
|
||||
z: (window.innerHeight - top + 2) / zoom,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appendToRef = computed(() => {
|
||||
return props.appendTo || 'body'
|
||||
})
|
||||
|
||||
onClickOutside(triggerRef, () => (isOpen.value = false))
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('scroll', onResize)
|
||||
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('scroll', onResize)
|
||||
})
|
||||
|
||||
const { slots } = context
|
||||
|
||||
function renderLabel() {
|
||||
const children = renderItem()
|
||||
if (children) {
|
||||
if (!Array.isArray(children)) {
|
||||
return <span class={['i-select-input'].concat(props.inputClass)}>{children}</span>
|
||||
} else if (children.length > 0) {
|
||||
return (
|
||||
<span class={['i-select-input space-x-1'].concat(props.inputClass)}>
|
||||
{renderList(children, (item) => (
|
||||
<span>{item}</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
const emptyLabel =
|
||||
typeof props.emptyLabel == 'function' ? props.emptyLabel() : props.emptyLabel
|
||||
return <span class="w-full pl-2 i-select-input i-select-empty-label">{emptyLabel}</span>
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div
|
||||
class={['min-w-20 w-40 i-select '].concat(props.highlight)}
|
||||
onClick={collapse}
|
||||
ref={selectRef}
|
||||
>
|
||||
<div
|
||||
class={{
|
||||
'i-select-trigger': true,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
ref={triggerRef}
|
||||
>
|
||||
{renderLabel()}
|
||||
<calc-button
|
||||
disabled={props.disabled}
|
||||
icon="dropdown"
|
||||
class="cursor-pointer"
|
||||
></calc-button>
|
||||
</div>
|
||||
<Teleport to={appendToRef.value}>
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
<div
|
||||
class="i-select-dropdown"
|
||||
style={dropdownStyle.value}
|
||||
v-show={isOpen.value}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
{renderSlot(slots, 'default')}
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:color';
|
||||
$text-color: #937639;
|
||||
.i-select {
|
||||
min-width: 80px;
|
||||
width: 160px;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
line-height: 100%;
|
||||
// font-size: 12px;
|
||||
color: $text-color;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
border: 1px solid #5b472a;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
padding-right: 3px;
|
||||
margin: 0;
|
||||
display: block;
|
||||
|
||||
.i-select-trigger {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.i-select-input {
|
||||
padding-left: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.i-select-empty-label {
|
||||
color: #5e5e5e;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.i-select-empty-label {
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.i-select-trigger:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: red !important;
|
||||
border: 1px solid #aa8651;
|
||||
}
|
||||
|
||||
.remind {
|
||||
color: #32e432 !important;
|
||||
border: 1px solid #aa8651;
|
||||
}
|
||||
|
||||
.i-select-dropdown {
|
||||
position: fixed;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
background: black;
|
||||
// font-size: 12px;
|
||||
z-index: 888;
|
||||
|
||||
$hoverColor: #002947;
|
||||
$activeColor: color.scale($hoverColor, $lightness: 5%);
|
||||
color: $text-color;
|
||||
|
||||
.i-select-dropdown-item {
|
||||
// font-size: 12px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
border: none;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.active {
|
||||
// background-color: $activeColor;
|
||||
color: #e9c556;
|
||||
}
|
||||
&.active::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
// font-size: 12px;
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script lang="tsx">
|
||||
import { HiSelection } from 'hoci'
|
||||
|
||||
export default HiSelection
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export const CHECKBOX_GROUP: InjectionKey<boolean> = Symbol('checkbox-group')
|
||||
@@ -0,0 +1,211 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, ref, computed, shallowRef, watch } from 'vue'
|
||||
import { useVModel, useDraggable, clamp } from '@vueuse/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ISlider',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showPercent: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
formatValue: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const buttonRef = shallowRef<HTMLDivElement | null>(null)
|
||||
const runwayRef = shallowRef<HTMLDivElement | null>(null)
|
||||
const modelValue = useVModel(props, 'modelValue', emit, { passive: true })
|
||||
|
||||
const percentage = computed(() => {
|
||||
return ((modelValue.value - props.min) / (props.max - props.min)) * 100
|
||||
})
|
||||
|
||||
useDraggable(buttonRef, {
|
||||
onMove(position) {
|
||||
if (!runwayRef.value || props.disabled) {
|
||||
return
|
||||
}
|
||||
const runwayRect = runwayRef.value?.getBoundingClientRect()
|
||||
|
||||
if (!runwayRect) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用runway的位置和宽度来计算百分比
|
||||
const percentage = Math.max(
|
||||
0,
|
||||
Math.min(100, ((position.x - runwayRect.left) / runwayRect.width) * 100),
|
||||
)
|
||||
|
||||
const range = props.max - props.min
|
||||
let value = (percentage * range) / 100 + props.min
|
||||
|
||||
// 应用步长
|
||||
value = Math.round(value / props.step) * props.step
|
||||
|
||||
// 确保值在范围内
|
||||
value = Math.max(props.min, Math.min(props.max, value))
|
||||
|
||||
modelValue.value = value
|
||||
emit('change', value)
|
||||
},
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div
|
||||
class={[
|
||||
'w-full h-8 relative cursor-pointer flex items-center gap-3 py-2',
|
||||
{ 'cursor-not-allowed': props.disabled },
|
||||
]}
|
||||
>
|
||||
<div class="flex-1 h-1 bg-[#4D524D] relative" ref={runwayRef}>
|
||||
<div
|
||||
class="h-1 bg-[#8E6D28] absolute left-0 top-0"
|
||||
style={{ width: `${percentage.value}%` }}
|
||||
></div>
|
||||
<div
|
||||
class="h-6 w-6 absolute top-1/2 -translate-y-1/2 bg-transparent text-center select-none"
|
||||
style={{ left: `calc(${percentage.value}% - 0.75rem)` }}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div
|
||||
class={[
|
||||
'w-4 h-4 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
'bg-gradient-to-b from-[#223768] to-[#122438]',
|
||||
'border border-solid border-[#937639] rounded-full transition-all duration-200',
|
||||
'shadow-[0_2px_4px_rgba(0,0,0,0.2)]',
|
||||
{ 'bg-gradient-to-b from-[#353535] to-[#1c1c1c] border-[#414141]': props.disabled },
|
||||
]}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{props.showPercent && (
|
||||
<div
|
||||
class={[
|
||||
'w-auto min-w-5 text-center text-sm font-medium',
|
||||
props.disabled ? 'text-[#696969]' : 'text-[#e9c556]',
|
||||
]}
|
||||
>
|
||||
{props.formatValue
|
||||
? props.formatValue(modelValue.value)
|
||||
: `${percentage.value.toFixed(0)}%`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.i-slider {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.i-slider.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.i-slider.is-disabled .i-slider-button {
|
||||
background: linear-gradient(#353535, #1c1c1c);
|
||||
border-color: #414141;
|
||||
}
|
||||
|
||||
.i-slider.is-disabled .i-slider-value {
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
.i-slider-runway {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background-color: #122438;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.i-slider-bar {
|
||||
height: 4px;
|
||||
background: #c6b083;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.i-slider-button-wrapper {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.i-slider-button {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(#223768, #122438);
|
||||
border: 1px solid #755f44;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.i-slider-button:hover {
|
||||
background: linear-gradient(#2d4e9f, #153051);
|
||||
border-color: #e3c18a;
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
|
||||
.i-slider-button:active {
|
||||
background: linear-gradient(#2d4e9f, #122438);
|
||||
border-color: #e3c18a;
|
||||
}
|
||||
|
||||
.i-slider-value {
|
||||
min-width: 45px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
color: #e9c556;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, renderSlot } from 'vue'
|
||||
import { HiItem, selectionProps } from 'hoci'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CalcTab',
|
||||
props: {
|
||||
...selectionProps,
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
return <HiItem {...props}>{renderSlot(slots, 'default')}</HiItem>
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
export const AciveSymbol = Symbol('[i-tabs]active')
|
||||
|
||||
export const AciveClassSymbol = Symbol('[i-tabs]active-class')
|
||||
|
||||
export const ChangeActiveSymbol = Symbol('[i-tabs]change-active')
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="tsx">
|
||||
import { HiSelection, selectionProps } from 'hoci'
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, defineComponent, renderSlot } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { parseQuery, useRouter } from 'vue-router'
|
||||
import type { BaseType } from '@/components/hooks/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CalcTabs',
|
||||
props: {
|
||||
...selectionProps,
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
},
|
||||
route: {
|
||||
type: Boolean,
|
||||
},
|
||||
query: {
|
||||
type: [Object, String] as PropType<string | LocationQuery>,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, slots }) {
|
||||
const router = useRouter()
|
||||
|
||||
const routeQuery = computed(() => {
|
||||
if (typeof props.query == 'string') {
|
||||
return parseQuery(props.query)
|
||||
}
|
||||
return props.query ?? {}
|
||||
})
|
||||
|
||||
const modelValue = computed<BaseType>({
|
||||
get() {
|
||||
let val = props.modelValue
|
||||
if (props.route) {
|
||||
val = val ?? router.currentRoute.value.path
|
||||
}
|
||||
return val
|
||||
},
|
||||
set(val) {
|
||||
if (props.route && val) {
|
||||
val = decodeURIComponent(val.toString())
|
||||
router.push({ path: val, query: routeQuery.value })
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<HiSelection
|
||||
v-model={modelValue.value}
|
||||
class={{ 'i-tabs': true, vertical: !!props.vertical }}
|
||||
activeClass="active"
|
||||
itemClass="i-tab cursor-pointer"
|
||||
>
|
||||
{renderSlot(slots, 'default')}
|
||||
</HiSelection>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-tabs {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
align-items: flex-end;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
color: #c6b083;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #c6b083;
|
||||
&:visited {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
|
||||
.i-tab {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.i-tab {
|
||||
// font-size: 12px;
|
||||
margin-left: 1px;
|
||||
width: fit-content;
|
||||
padding: 0px 10px;
|
||||
padding-bottom: 2px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
box-sizing: border-box;
|
||||
|
||||
text-shadow: none;
|
||||
|
||||
&:visited {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
border: #4f4838 1px solid;
|
||||
border-bottom: none;
|
||||
border-radius: 5px 5px 0 0;
|
||||
background: linear-gradient(to top, rgba(6, 6, 6, 0.9), rgba(52, 52, 52, 0.9));
|
||||
transition: all 0.4s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(#4a4b4e, #1d1e22);
|
||||
}
|
||||
|
||||
&.active {
|
||||
margin-bottom: -1px;
|
||||
background: linear-gradient(#574d38, #25221d);
|
||||
border-image: url('@/assets/img/control/active_top.png') 1 fill stretch;
|
||||
height: 22px;
|
||||
// color: #ffb400;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
filter: grayscale(100%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent } from 'vue'
|
||||
import { NTooltip } from '@/components/base'
|
||||
|
||||
import { tooltipProps } from '@/components/base/tooltip'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ITooltip',
|
||||
components: { NTooltip },
|
||||
props: {
|
||||
...tooltipProps,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
return (
|
||||
<n-tooltip
|
||||
{...props}
|
||||
pop-class="i-popper z-999 fixed top-0 left-0 bg-black bg-opacity-90 border-1.5 border-hex-272928 rounded-2px border-solid"
|
||||
>
|
||||
{slots}
|
||||
</n-tooltip>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 322 B |
@@ -0,0 +1,105 @@
|
||||
<script lang="tsx">
|
||||
import { HiSelection, selectionProps } from 'hoci'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, defineComponent, renderList, watch } from 'vue'
|
||||
import CalcTreeNode from './node.vue'
|
||||
import type { TreeNode } from './types'
|
||||
import { labelPropType, valuePropType } from '@/components/hooks/types'
|
||||
|
||||
const Tree = defineComponent({
|
||||
name: 'CalcTree',
|
||||
props: {
|
||||
...selectionProps,
|
||||
modelValue: {
|
||||
type: valuePropType,
|
||||
default: () => null,
|
||||
},
|
||||
activeItem: {
|
||||
type: Object as PropType<TreeNode>,
|
||||
default: () => null,
|
||||
},
|
||||
idKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
label: {
|
||||
type: labelPropType,
|
||||
},
|
||||
data: {
|
||||
type: Array as PropType<TreeNode[]>,
|
||||
default: () => [],
|
||||
},
|
||||
depth: {
|
||||
type: [Number, String] as PropType<number>,
|
||||
default: () => 0,
|
||||
},
|
||||
depthStyle: {
|
||||
type: Function as PropType<(depth: number) => string>,
|
||||
},
|
||||
},
|
||||
emits: ['select', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const modelValue = useVModel(props, 'modelValue')
|
||||
|
||||
function onSelect(item?: TreeNode) {
|
||||
if (item) {
|
||||
emit('select', item)
|
||||
item.onSelect?.()
|
||||
}
|
||||
}
|
||||
|
||||
function loadItem(total: TreeNode[], items: TreeNode[]) {
|
||||
items?.forEach((item) => {
|
||||
if (item.children) {
|
||||
loadItem(total, item.children)
|
||||
}
|
||||
total.push(item)
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
const children = computed(() => {
|
||||
return loadItem([], props.data)
|
||||
})
|
||||
|
||||
watch(modelValue, (val) => {
|
||||
val = val ?? props.defaultValue
|
||||
const item = children.value.find((item) => item.value === val)
|
||||
onSelect(item)
|
||||
})
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<HiSelection
|
||||
{...props}
|
||||
activeClass="i-tree-node-active"
|
||||
v-model={modelValue.value}
|
||||
class="i-tree"
|
||||
>
|
||||
{renderList(props.data, (item) => {
|
||||
return (
|
||||
<CalcTreeNode
|
||||
{...item}
|
||||
onSelect={onSelect}
|
||||
depth-style={props.depthStyle}
|
||||
depth={props.depth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</HiSelection>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Tree
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.i-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script lang="tsx">
|
||||
import { HiItem } from 'hoci'
|
||||
import type { PropType } from 'vue'
|
||||
import { Transition, computed, defineComponent, ref, renderList } from 'vue'
|
||||
import type { TreeNode } from './types'
|
||||
|
||||
const LegacySelectionItem = HiItem as any
|
||||
|
||||
const NTreeNode = defineComponent({
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object, Array] as PropType<any>,
|
||||
default: () => null,
|
||||
},
|
||||
children: {
|
||||
type: Array as PropType<TreeNode[]>,
|
||||
default: () => [],
|
||||
},
|
||||
depth: {
|
||||
type: [String, Number] as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
depthStyle: {
|
||||
type: Function as PropType<(depth: number) => string>,
|
||||
},
|
||||
depthOffset: {
|
||||
type: String,
|
||||
default: '12px',
|
||||
},
|
||||
},
|
||||
emits: ['select'],
|
||||
setup(props, { emit }) {
|
||||
const isOpen = ref(false)
|
||||
|
||||
function open() {
|
||||
isOpen.value = !isOpen.value
|
||||
onSelect(props)
|
||||
}
|
||||
|
||||
function onSelect(item: TreeNode) {
|
||||
return emit('select', item)
|
||||
}
|
||||
|
||||
const depthStyle = computed(() => {
|
||||
if (props.depthStyle) {
|
||||
return props.depthStyle(props.depth)
|
||||
}
|
||||
return `padding-left: calc(${props.depth}*${props.depthOffset} + 12px);`
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="w-full i-tree-item relative">
|
||||
<LegacySelectionItem
|
||||
label={props.label}
|
||||
value={props.value}
|
||||
style={depthStyle.value}
|
||||
class="flex i-tree-item-label items-center"
|
||||
onClick={open}
|
||||
>
|
||||
<div class="top-0 right-0 bottom-0 left-0 absolute i-tree-item-active-mask"></div>
|
||||
<div class="top-0 right-0 bottom-0 left-0 absolute i-tree-item-mask"></div>
|
||||
<calc-button
|
||||
v-show={props.children?.length}
|
||||
icon={isOpen.value ? 'collapse-up' : 'collapse-down'}
|
||||
></calc-button>
|
||||
<div class="pl-1">{props.label}</div>
|
||||
</LegacySelectionItem>
|
||||
<Transition name="dropdown" mode="out-in">
|
||||
<div v-show={isOpen.value}>
|
||||
{renderList(props.children, (item) => {
|
||||
return <NTreeNode onSelect={onSelect} {...item} depth={Number(props.depth) + 1} />
|
||||
})}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default NTreeNode
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-tree-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
|
||||
border: 1px solid transparent;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.i-tree-item-label {
|
||||
flex: 1;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
color: #ddc593;
|
||||
position: relative;
|
||||
&:hover {
|
||||
.i-tree-item-mask {
|
||||
background: url('./img/item_hover.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.i-tree-node-active {
|
||||
.i-tree-item-active-mask {
|
||||
background: url('./img/item_active.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-collapse-up {
|
||||
background-image: url('./img/collapse_up.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 13px !important;
|
||||
width: 13px !important;
|
||||
height: 13px !important;
|
||||
}
|
||||
|
||||
.icon-collapse-down {
|
||||
background-image: url('./img/collapse_down.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 13px !important;
|
||||
width: 13px !important;
|
||||
height: 13px !important;
|
||||
}
|
||||
|
||||
.i-tree-dropdown {
|
||||
width: 100%;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
animation: dropdown-enter 0.2s ease-in;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
animation: dropdown-leave 0.2s ease-in;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface TreeNode {
|
||||
label: string
|
||||
id?: string | number
|
||||
value?: any
|
||||
children?: TreeNode[]
|
||||
onSelect?: () => void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface TreeData {}
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="pos-relative w-300px h-full">
|
||||
<template v-for="part in infoStore.avatarParts" :key="part">
|
||||
<div
|
||||
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
|
||||
:style="infoStyle(part)"
|
||||
@click="choosePart(part)"
|
||||
>
|
||||
{{ partName(part) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="EquList">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useInfoStore } from '@/stores'
|
||||
|
||||
const props = defineProps({
|
||||
part: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const partModelValue = useVModel(props, 'part')
|
||||
const infoStore = useInfoStore()
|
||||
|
||||
const infoStyle = (part: string) => {
|
||||
let x = 15
|
||||
let y = 5
|
||||
const index = infoStore.avatarParts.findIndex((p) => p == part)
|
||||
// if (index == 13) {
|
||||
// index -= 7
|
||||
// } else if (index >= 5 && index <= 12) {
|
||||
// x += 179
|
||||
// index -= 5
|
||||
// }
|
||||
if (part == '宠物') {
|
||||
x += 5 * 39
|
||||
y += 1 * 40
|
||||
} else if (part.includes('宠物装备')) {
|
||||
x += 6 * 39
|
||||
y += (index - infoStore.avatarParts.findIndex((p) => p == '宠物装备-红')) * 40
|
||||
} else {
|
||||
x += (index % 4) * 39
|
||||
y += Math.floor(index / 4) * 40
|
||||
}
|
||||
|
||||
const style = {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
const choosePart = (part: string) => {
|
||||
partModelValue.value = part
|
||||
}
|
||||
|
||||
const partName = (part: string) => {
|
||||
if (part == '武器装扮') return '武器'
|
||||
if (part == '快捷栏装备') return '快捷'
|
||||
return part.replace('宠物装备-', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
After Width: | Height: | Size: 172 B |
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 119 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="pos-relative w-248px h-full" :class="props.withBg ? 'emblem-bg' : ''">
|
||||
<template v-for="part in parts" :key="part">
|
||||
<div
|
||||
class="pos-absolute text-white/50 flex items-center justify-center rounded-50%"
|
||||
:style="infoStyle(part)"
|
||||
@click="choosePart(part)"
|
||||
>
|
||||
<img
|
||||
class="w-25px h-25px rounded-50%"
|
||||
style="object-fit: none; object-position: hue; mix-blend-mode: screen"
|
||||
:src="rarityIcon(part)"
|
||||
v-if="part != '白金'"
|
||||
/>
|
||||
<img class="pos-absolute" :style="lvStyle" :src="lvIcon(part)" v-if="part != '白金'" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="EmblemList">
|
||||
import { useInfoStore, type IConfigEmblem } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
|
||||
const props = defineProps({
|
||||
withBg: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
detail: {
|
||||
type: Object as PropType<Record<string, IConfigEmblem>>,
|
||||
},
|
||||
})
|
||||
|
||||
const infoStore = useInfoStore()
|
||||
const partModelValue = defineModel<string>('part', { default: '彩色' })
|
||||
|
||||
const partIcon = computed(() => {
|
||||
return (part: string) => {
|
||||
return new URL(`../Images/part/${part}.png`, import.meta.url).href
|
||||
}
|
||||
})
|
||||
|
||||
const rarityIcon = computed(() => {
|
||||
return (part: string) => {
|
||||
const rarity = Math.floor((props.detail?.[part]?.lv || 0) / 5)
|
||||
// console.log(rarity)
|
||||
return new URL(`../Images/rarity/${rarity}.png`, import.meta.url).href
|
||||
}
|
||||
})
|
||||
|
||||
const lvIcon = computed(() => {
|
||||
return (part: string) => {
|
||||
const lv = (props.detail?.[part]?.lv || 0) % 5
|
||||
return getImageURL(`/equipment/adaptation/${lv + 1}.png`)
|
||||
}
|
||||
})
|
||||
|
||||
const position: Record<string, { x: number; y: number; width?: number; height?: number }> = {
|
||||
彩色: {
|
||||
x: 99,
|
||||
y: 15,
|
||||
},
|
||||
红色: {
|
||||
x: 18,
|
||||
y: 39,
|
||||
},
|
||||
蓝色: {
|
||||
x: 180,
|
||||
y: 39,
|
||||
},
|
||||
黄色: {
|
||||
x: 65,
|
||||
y: 83,
|
||||
},
|
||||
绿色: {
|
||||
x: 133,
|
||||
y: 83,
|
||||
},
|
||||
白金: {
|
||||
x: 33,
|
||||
y: 117,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
增益: {
|
||||
x: 192,
|
||||
y: 117,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
}
|
||||
|
||||
const lvStyle = computed(() => {
|
||||
return {
|
||||
left: '50%',
|
||||
top: '100%',
|
||||
transform: 'translate(-50%,-100%)',
|
||||
zIndex: 7,
|
||||
}
|
||||
})
|
||||
|
||||
const infoStyle = computed(() => {
|
||||
return (part: string) => {
|
||||
console.log(`url(${rarityIcon.value(part)}),url(${partIcon.value(part)})`)
|
||||
return {
|
||||
left: `${position[part as keyof typeof position].x}px`,
|
||||
top: `${position[part as keyof typeof position].y}px`,
|
||||
width: `${position[part as keyof typeof position]?.width ?? 50}px`,
|
||||
height: `${position[part as keyof typeof position]?.height ?? 50}px`,
|
||||
backgroundImage: `url(${partIcon.value(part)})`,
|
||||
// backgroundImage: `url(${rarityIcon.value(part)}),url(${partIcon.value(part)})`,
|
||||
// backgroundImage: `url(${partIcon.value(part)}),url(${rarityIcon.value(part)})`,
|
||||
// backgroundBlendMode: 'screen',
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const parts = computed(() => {
|
||||
const parts = infoStore.emblemParts
|
||||
return parts
|
||||
})
|
||||
|
||||
const choosePart = (part: string) => {
|
||||
partModelValue.value = part
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.emblem-bg {
|
||||
background-image: url(../Images/head.png);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<template v-if="props.equipment">
|
||||
<div class="relative w-28px h-28px shrink-0" :class="{ inactive: props.inactive }">
|
||||
<div
|
||||
class="w-28px h-28px"
|
||||
:style="{
|
||||
backgroundImage: `url(${getImageURL((useDisplayUrl ? props.equipment?.displayUrl : props.equipment?.imageUrl) || '')})`,
|
||||
}"
|
||||
>
|
||||
<img :src="getLocalImageURL(rarity)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="box-border w-28px h-28px shrink-0 border-1px border-solid border-#7a7a7a/50"></div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
|
||||
const props = defineProps<{
|
||||
equipment?: IEquipment
|
||||
inactive?: boolean
|
||||
useDisplayUrl?: boolean
|
||||
}>()
|
||||
|
||||
const rarity = computed(() => {
|
||||
switch (props.equipment?.rarity) {
|
||||
case '稀有':
|
||||
return 'rare'
|
||||
case '神器':
|
||||
return 'artifact'
|
||||
case '传说':
|
||||
return 'legend'
|
||||
case '史诗':
|
||||
return 'epic'
|
||||
case '太初':
|
||||
return 'primeval'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const getLocalImageURL = (rarity: string) => {
|
||||
return new URL(`./rarity/${rarity}.png`, import.meta.url).href
|
||||
}
|
||||
</script>
|
||||
``
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @keyframes primeval {
|
||||
// @for $i from 0 through 29 {
|
||||
// #{percentage($i / 29)} {
|
||||
// background-image: url("./effect/primeval/#{$i}.png");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .primeval_effect {
|
||||
// width: 28px;
|
||||
// height: 28px;
|
||||
// background-size: cover;
|
||||
// animation: primeval 2s infinite;
|
||||
// // 设置动画名称,持续时间,和循环次数
|
||||
// mix-blend-mode: color-dodge;
|
||||
// }
|
||||
|
||||
.inactive {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: #00000090;
|
||||
// filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 300 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 293 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 316 B |
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="bg-hex-222222">
|
||||
<div
|
||||
:class="['bg-black text-shadow text-12px h-auto', 'text-#9D9476']"
|
||||
style="
|
||||
width: 220px;
|
||||
float: left;
|
||||
padding: 5px;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(0, 0, 0);
|
||||
"
|
||||
>
|
||||
<div class="h-30px flex items-left my-2px">
|
||||
<div style="position: relative" class="w-28px mr-2px">
|
||||
<Icon :equipment="props.equipment"></Icon>
|
||||
</div>
|
||||
<div style="line-height: 15px">
|
||||
<div :class="[rarity, 'pl-5px']">{{ props.equipment?.name }}</div>
|
||||
<div class="pl-5px text-#3ea74e">{{ props.equipment?.categorize }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 装备图标 装备名称 -->
|
||||
|
||||
<div class="divide"></div>
|
||||
<div class="flex flex-col items-end my-1px" style="line-height: 15px">
|
||||
<div :class="rarity">{{ props.equipment?.rarity }}</div>
|
||||
<div class="text-#7C6430">{{ props.equipment?.itemDetailType }}</div>
|
||||
</div>
|
||||
<!-- 装备基础信息 -->
|
||||
<div class="divide"></div>
|
||||
<div
|
||||
class="flex flex-col items-start my-1px"
|
||||
style="white-space: pre-wrap; line-height: 15px"
|
||||
>
|
||||
<div class="text-#908150 py-4px" v-html="mainAttr"></div>
|
||||
</div>
|
||||
<!-- 装备属性 -->
|
||||
<div class="divide"></div>
|
||||
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
|
||||
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div>
|
||||
</div>
|
||||
<template v-if="bufferDetail">
|
||||
<div class="divide"></div>
|
||||
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
|
||||
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div>
|
||||
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 奶系专属 -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import Icon from '../Icon/index.vue'
|
||||
import { formatAttr, rarityClass } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
equipment?: IEquipment
|
||||
}>()
|
||||
|
||||
const rarity = computed(() => rarityClass(props.equipment?.rarity ?? ''))
|
||||
|
||||
const mainAttr = computed(() => {
|
||||
const attrs: (keyof IEquipment)[] = [
|
||||
'STR',
|
||||
'INT',
|
||||
'Vitality',
|
||||
'Spirit',
|
||||
'AtkP',
|
||||
'AtkM',
|
||||
'AtkI',
|
||||
'SkillAttack',
|
||||
'Attack',
|
||||
'Buffer',
|
||||
]
|
||||
let info = ''
|
||||
for (const attr of attrs) {
|
||||
const item = props.equipment?.[attr] as number[]
|
||||
if (item?.[0]) {
|
||||
info += `${formatAttr(attr, item?.[0])}\n`
|
||||
}
|
||||
}
|
||||
return info
|
||||
})
|
||||
|
||||
// a.match(/\[(?!.*?技能).*?]/g)
|
||||
|
||||
const detail = computed(() =>
|
||||
props.equipment?.detail?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
),
|
||||
)
|
||||
|
||||
const bufferDetail = computed(() => {
|
||||
const detail = props.equipment?.bufferDetail
|
||||
if (!detail || detail == '-') return undefined
|
||||
return props.equipment?.bufferDetail.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divide {
|
||||
border-top: 1px solid rgba(243, 244, 246, 0.3);
|
||||
margin: 2px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div
|
||||
class="pos-relative w-264px h-full"
|
||||
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/head.png')})` : 'none' }"
|
||||
>
|
||||
<template v-for="part in parts" :key="part">
|
||||
<div
|
||||
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
|
||||
:style="infoStyle(part)"
|
||||
@click="choosePart(part)"
|
||||
>
|
||||
<template v-if="detail?.[part]?.id">
|
||||
<EquipmentIcon :equipment="equ(detail[part].id)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ partName(part) }}
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
|
||||
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
|
||||
</div> -->
|
||||
</template>
|
||||
<div
|
||||
v-if="props.withSubWeapon"
|
||||
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
|
||||
style="left: 150px; top: 5px; z-index: 8"
|
||||
@click="choosePart('副武器')"
|
||||
>
|
||||
<template v-if="detail?.['副武器']?.id">
|
||||
<EquipmentIcon :equipment="equ(detail['副武器'].id)" />
|
||||
</template>
|
||||
<template v-else> 副武 </template>
|
||||
</div>
|
||||
<img
|
||||
v-if="props.avatar"
|
||||
:src="props.avatar"
|
||||
alt="Avatar"
|
||||
class="pos-absolute w-70% left-1/2 translate-x--1/2 -bottom-10px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="EquList">
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
import { useInfoStore, useConfigStore, type IConfigEquip } from '@/stores'
|
||||
|
||||
const props = defineProps({
|
||||
part: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
withBg: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
withPet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
detail: {
|
||||
type: Object as PropType<Record<string, IConfigEquip>>,
|
||||
},
|
||||
withSubWeapon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const partModelValue = useVModel(props, 'part')
|
||||
const infoStore = useInfoStore()
|
||||
|
||||
const parts = computed(() => {
|
||||
const configStore = useConfigStore()
|
||||
const parts = infoStore.parts.filter((part) => {
|
||||
if (part == '宠物' && !props.withPet) return false
|
||||
if (configStore.config.equips[part]) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return parts
|
||||
})
|
||||
|
||||
const equ = (id: string) => {
|
||||
return infoStore.equips.find((e) => e.id == id)
|
||||
}
|
||||
|
||||
// const oaths = (id: string) => {
|
||||
// return infoStore.oaths.find((e) => e.id == id)
|
||||
// }
|
||||
|
||||
const infoStyle = (part: string) => {
|
||||
let x = 10
|
||||
let y = 10
|
||||
let index = infoStore.parts.findIndex((p) => p == part)
|
||||
if (index == 13) {
|
||||
index -= 7
|
||||
} else if (index >= 5 && index <= 12) {
|
||||
x += 179
|
||||
index -= 5
|
||||
}
|
||||
|
||||
x += (index % 2) * 39
|
||||
y += Math.floor(index / 2) * 34
|
||||
|
||||
const style = {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// const fusionStyle = (part: string) => {
|
||||
// let x = 0
|
||||
// let y = 15
|
||||
// let index = infoStore.parts.findIndex((p) => p == part)
|
||||
|
||||
// if (index >= 5 && index <= 12) {
|
||||
// x += 179
|
||||
// index -= 5
|
||||
// }
|
||||
|
||||
// if (index == 13) {
|
||||
// index = 5
|
||||
// }
|
||||
|
||||
// x += (index % 2) * 39
|
||||
// y += Math.floor(index / 2) * 34
|
||||
|
||||
// return {
|
||||
// left: `${x}px`,
|
||||
// top: `${y}px`,
|
||||
// zIndex: 7,
|
||||
// transform: 'scale(0.6)',
|
||||
// }
|
||||
// }
|
||||
const choosePart = (part: string) => {
|
||||
partModelValue.value = part
|
||||
}
|
||||
|
||||
const partName = (part: string) => {
|
||||
if (part == '辅助装备') return '左槽'
|
||||
if (part == '魔法石') return '右槽'
|
||||
return part
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div
|
||||
class="pos-relative w-264px h-full"
|
||||
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/oath/head.png')})` : 'none' }"
|
||||
>
|
||||
<template v-for="part in parts" :key="part">
|
||||
<div
|
||||
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
|
||||
:style="infoStyle(part)"
|
||||
@click="choosePart(part)"
|
||||
>
|
||||
<template v-if="detail?.[part]?.id">
|
||||
<EquipmentIcon :equipment="equ(detail[part].id, part)" useDisplayUrl />
|
||||
</template>
|
||||
<template v-else-if="!withBg">
|
||||
{{ part != '11' ? '星蕴' : '誓约' }}
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
|
||||
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
|
||||
</div> -->
|
||||
</template>
|
||||
<!-- <img
|
||||
v-if="props.avatar"
|
||||
:src="props.avatar"
|
||||
alt="Avatar"
|
||||
class="pos-absolute w-70% left-1/2 translate-x--1/2 -bottom-10px"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="EquList">
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
import { useInfoStore, type IConfigOath } from '@/stores'
|
||||
|
||||
const props = defineProps({
|
||||
withBg: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
detail: {
|
||||
type: Object as PropType<Record<string, IConfigOath>>,
|
||||
},
|
||||
})
|
||||
const partModelValue = defineModel<string>('part', { default: '0' })
|
||||
const infoStore = useInfoStore()
|
||||
|
||||
const parts = computed(() => {
|
||||
const parts = infoStore.oathParts
|
||||
return parts
|
||||
})
|
||||
|
||||
const equ = (id: string, part: string) => {
|
||||
return infoStore.oaths.find((e) => e.id == id && e.oathPos == part)
|
||||
}
|
||||
|
||||
const position = [
|
||||
{
|
||||
x: 30,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 46,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 80.5,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 46,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 80.5,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 74,
|
||||
y: 131.5,
|
||||
},
|
||||
{
|
||||
x: 118,
|
||||
y: 145.5,
|
||||
},
|
||||
{
|
||||
x: 162,
|
||||
y: 131.5,
|
||||
},
|
||||
{
|
||||
x: 118,
|
||||
y: 111,
|
||||
},
|
||||
]
|
||||
|
||||
const positionSimple = [
|
||||
{
|
||||
x: 30,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 46,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 80.5,
|
||||
},
|
||||
{
|
||||
x: 30,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 46,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 80.5,
|
||||
},
|
||||
{
|
||||
x: 206,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 74,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 118,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 162,
|
||||
y: 114,
|
||||
},
|
||||
{
|
||||
x: 118,
|
||||
y: 80.5,
|
||||
},
|
||||
]
|
||||
|
||||
const infoStyle = (part: string) => {
|
||||
if (props.withBg)
|
||||
return {
|
||||
left: `${position[parseInt(part)].x}px`,
|
||||
top: `${position[parseInt(part)].y}px`,
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
}
|
||||
else
|
||||
return {
|
||||
left: `${positionSimple[parseInt(part)].x}px`,
|
||||
top: `${positionSimple[parseInt(part)].y}px`,
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
}
|
||||
}
|
||||
|
||||
const choosePart = (part: string) => {
|
||||
partModelValue.value = part
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<calc-cascader
|
||||
class="!h-5 flex-1"
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
placeholder="请输入名称搜索"
|
||||
></calc-cascader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TreeNode } from '@/components/calc/tree/types'
|
||||
import { useInfoStore } from '@/stores'
|
||||
import { syncRef, useVModel } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
partList: {
|
||||
type: Array as () => string[],
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const armor_parts = ['上衣', '头肩', '下装', '鞋', '腰带']
|
||||
|
||||
const jewel_parts = ['项链', '戒指', '手镯']
|
||||
|
||||
const special_parts = ['辅助装备', '魔法石', '耳环']
|
||||
|
||||
const else_parts = ['称号', '宠物']
|
||||
|
||||
const emit = defineEmits([])
|
||||
|
||||
const choose_part = useVModel(props, 'partList', emit, { passive: true })
|
||||
|
||||
const selected = ref('')
|
||||
|
||||
const infoStore = useInfoStore()
|
||||
|
||||
const items = computed<TreeNode[]>(() => {
|
||||
return [
|
||||
{
|
||||
label: '全部类别',
|
||||
value: '',
|
||||
onSelect() {
|
||||
choose_part.value = []
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '武器',
|
||||
value: '武器',
|
||||
onSelect() {
|
||||
choose_part.value = infoStore.infos?.weapons ?? []
|
||||
},
|
||||
children: infoStore.infos?.weapons.map((r) => {
|
||||
return {
|
||||
label: r,
|
||||
value: r,
|
||||
onSelect() {
|
||||
choose_part.value = [r]
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '防具',
|
||||
value: '防具',
|
||||
onSelect() {
|
||||
choose_part.value = [...armor_parts]
|
||||
},
|
||||
children: armor_parts.map((part) => {
|
||||
return {
|
||||
label: part,
|
||||
value: part,
|
||||
onSelect() {
|
||||
choose_part.value = [part]
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '首饰',
|
||||
value: '首饰',
|
||||
onSelect() {
|
||||
choose_part.value = [...jewel_parts]
|
||||
},
|
||||
children: jewel_parts.map((part) => {
|
||||
return {
|
||||
label: part,
|
||||
value: part,
|
||||
onSelect() {
|
||||
choose_part.value = [part]
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '特殊装备',
|
||||
value: '特殊装备',
|
||||
onSelect() {
|
||||
choose_part.value = [...special_parts]
|
||||
},
|
||||
children: special_parts.map((part) => {
|
||||
return {
|
||||
label: part,
|
||||
value: part,
|
||||
onSelect() {
|
||||
choose_part.value = [part]
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '其它',
|
||||
value: '其它',
|
||||
onSelect() {
|
||||
choose_part.value = [...else_parts]
|
||||
},
|
||||
children: else_parts.map((part) => {
|
||||
return {
|
||||
label: part,
|
||||
value: part,
|
||||
onSelect() {
|
||||
choose_part.value = [part]
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="flex gap-8px">
|
||||
<template v-for="(rarity, _) in rarities" :key="_">
|
||||
<div
|
||||
class="w-14px h-14px shadow-[0px_0px_3px_#ffffff_inset,0px_0px_1px_#ffffff40]"
|
||||
:style="{ background: rarity.background }"
|
||||
@click="choose(rarity.value)"
|
||||
>
|
||||
<div class="w-14px h-14px" :class="{ active: value === rarity.value }"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({})
|
||||
|
||||
const value = defineModel<string>()
|
||||
|
||||
const rarities = [
|
||||
{
|
||||
value: '稀有',
|
||||
background: '#9879da',
|
||||
},
|
||||
{
|
||||
value: '神器',
|
||||
background: '#ef28c1',
|
||||
},
|
||||
{
|
||||
value: '传说',
|
||||
background: '#fc8711',
|
||||
},
|
||||
{
|
||||
value: '史诗',
|
||||
background: '#fcb600',
|
||||
},
|
||||
{
|
||||
value: '太初',
|
||||
background: 'linear-gradient(to bottom, #58fa84 20%, #58decc 40%, #58c6ff 60%)',
|
||||
},
|
||||
]
|
||||
|
||||
const choose = (rarity: string) => {
|
||||
if (value.value === rarity) {
|
||||
value.value = undefined
|
||||
} else {
|
||||
value.value = rarity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active {
|
||||
background: url('@/assets/img/control/item_checked.png') no-repeat center;
|
||||
background-size: 12px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="bg-hex-222222">
|
||||
<div
|
||||
:class="['bg-black text-shadow text-12px h-auto', 'text-#9D9476']"
|
||||
style="
|
||||
width: 220px;
|
||||
float: left;
|
||||
padding: 5px;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(0, 0, 0);
|
||||
"
|
||||
>
|
||||
<!-- 装备图标 装备名称 -->
|
||||
<div class="flex w-full items-center justify-center gap-5px">
|
||||
<div>
|
||||
<img
|
||||
:src="getImageURL('equipment' + props.suit?.imageUrl.replace('/suit/', '/suit/icon/'))"
|
||||
alt="装备图标"
|
||||
/>
|
||||
</div>
|
||||
<div :class="rarity">{{ props.suit?.name }}</div>
|
||||
<div :class="rarity">{{ props.suit?.point }}点</div>
|
||||
</div>
|
||||
<div class="divide"></div>
|
||||
<!-- 装备基础信息 -->
|
||||
<div
|
||||
class="flex flex-col items-start my-1px"
|
||||
style="white-space: pre-wrap; line-height: 15px"
|
||||
>
|
||||
<div class="text-#908150" v-html="mainAttr"></div>
|
||||
</div>
|
||||
<!-- 装备属性 -->
|
||||
<div class="divide"></div>
|
||||
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
|
||||
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div>
|
||||
</div>
|
||||
|
||||
<template v-if="bufferDetail">
|
||||
<div class="divide"></div>
|
||||
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
|
||||
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div>
|
||||
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 奶系专属 -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment, ISuit } from '@/api/info/type'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import { formatAttr, rarityClass } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
suit?: ISuit
|
||||
}>()
|
||||
|
||||
const rarity = computed(() => rarityClass(props.suit?.rarity ?? ''))
|
||||
|
||||
const mainAttr = computed(() => {
|
||||
const attrs: (keyof ISuit)[] = ['SkillAttack', 'Buffer']
|
||||
let info = ''
|
||||
for (const attr of attrs) {
|
||||
const item = props.suit?.[attr] as number
|
||||
if (item) {
|
||||
info += `${formatAttr(attr, item)}\n`
|
||||
}
|
||||
}
|
||||
return info
|
||||
})
|
||||
|
||||
const detail = computed(() =>
|
||||
props.suit?.value?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
),
|
||||
)
|
||||
|
||||
const bufferDetail = computed(() => {
|
||||
const detail = props.suit?.bufferValue
|
||||
if (!detail || detail == '-') return undefined
|
||||
return props.suit?.bufferValue?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divide {
|
||||
border-top: 1px solid rgba(243, 244, 246, 0.3);
|
||||
margin: 2px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
通用的函数式组件
|
||||
@@ -0,0 +1,214 @@
|
||||
import { h, reactive, renderList } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import ActionDialog from '@/components/calc/action-dialog/index.vue'
|
||||
import type { JSX } from 'vue/jsx-runtime'
|
||||
|
||||
export type ConfirmDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'>
|
||||
export type AlertDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'>
|
||||
export type ElementLike = JSX.Element | string | ElementLike[]
|
||||
|
||||
function randomId() {
|
||||
return Symbol(Math.random().toString(36).slice(2, 9))
|
||||
}
|
||||
|
||||
type DialogID = string | number | symbol
|
||||
|
||||
export interface ShowDialogOption {
|
||||
id?: DialogID
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
header?: string | boolean
|
||||
/**
|
||||
* 内容插槽
|
||||
*/
|
||||
content?: (() => ElementLike) | ElementLike
|
||||
/**
|
||||
* 操作区插槽
|
||||
*/
|
||||
action?: (() => ElementLike) | ElementLike
|
||||
|
||||
cache?: boolean
|
||||
|
||||
/**
|
||||
* 超时自动关闭
|
||||
*/
|
||||
timeout?: number
|
||||
|
||||
/**
|
||||
* 关闭时的延迟动画
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* 确定按钮文本 为false时不显示
|
||||
*/
|
||||
okButton?: string | boolean
|
||||
/**
|
||||
* 取消按钮文本 为false时不显示
|
||||
*/
|
||||
cancelButton?: string | boolean
|
||||
/**
|
||||
* 拒绝按钮文本 为false时不显示
|
||||
*/
|
||||
rejectButton?: string | boolean
|
||||
|
||||
// 默认状态
|
||||
defaultStatus?: DialogResult['status']
|
||||
|
||||
mask?: boolean
|
||||
|
||||
scale?: number
|
||||
}
|
||||
|
||||
export interface DialogInstance {
|
||||
id?: DialogID
|
||||
visible: boolean
|
||||
render: (visible?: boolean) => ElementLike
|
||||
resolve: (result: DialogResult) => void
|
||||
}
|
||||
|
||||
export interface DialogResult {
|
||||
id: DialogID
|
||||
status: 'ok' | 'reject' | 'cancel' | 'none'
|
||||
data?: any
|
||||
readonly isOk: boolean
|
||||
readonly isCancel: boolean
|
||||
readonly isReject: boolean
|
||||
}
|
||||
|
||||
export const useDialog = createSharedComposable(() => {
|
||||
const dialogs = reactive(new Map<DialogID, DialogInstance>())
|
||||
|
||||
// 注册窗口
|
||||
function open(dialog: DialogInstance) {
|
||||
const id = dialog.id ?? randomId()
|
||||
dialogs.set(id, dialog)
|
||||
return id
|
||||
}
|
||||
|
||||
function close(id: DialogID, status = 'cancel', duration = 400) {
|
||||
const dialog = dialogs.get(id)
|
||||
if (!dialog) {
|
||||
return
|
||||
}
|
||||
dialog.resolve({
|
||||
id,
|
||||
status: 'ok',
|
||||
get isOk() {
|
||||
return status == 'ok'
|
||||
},
|
||||
get isCancel() {
|
||||
return status == 'cancel'
|
||||
},
|
||||
get isReject() {
|
||||
return status == 'reject'
|
||||
},
|
||||
})
|
||||
dialog.visible = false
|
||||
const fn = () => dialogs.delete(id)
|
||||
if (duration > 0) {
|
||||
setTimeout(fn, duration)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
return renderList(dialogs.values(), (e) => e.render(e.visible))
|
||||
}
|
||||
|
||||
async function show(option: ShowDialogOption = {}) {
|
||||
return new Promise<DialogResult>((resolve) => {
|
||||
const {
|
||||
id = randomId(),
|
||||
header = true,
|
||||
content,
|
||||
action,
|
||||
timeout,
|
||||
okButton = false,
|
||||
cancelButton = false,
|
||||
rejectButton = false,
|
||||
defaultStatus = 'none',
|
||||
duration = 400,
|
||||
mask = false,
|
||||
scale = 1.0,
|
||||
} = option
|
||||
|
||||
const onClose = (status: DialogResult['status'] = defaultStatus) => {
|
||||
close(id, status, duration)
|
||||
}
|
||||
|
||||
open({
|
||||
id,
|
||||
visible: true,
|
||||
resolve,
|
||||
render(visible = true) {
|
||||
return h(
|
||||
ActionDialog,
|
||||
{
|
||||
header,
|
||||
okButton,
|
||||
cancelButton,
|
||||
rejectButton,
|
||||
visible,
|
||||
onClose,
|
||||
mask,
|
||||
scale,
|
||||
},
|
||||
{
|
||||
default() {
|
||||
switch (typeof content) {
|
||||
case 'function':
|
||||
return content()
|
||||
case 'string':
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'w-full justify-center text-#d4d6b6 text-center',
|
||||
style: 'margin:10px',
|
||||
},
|
||||
content,
|
||||
)
|
||||
default:
|
||||
return content
|
||||
}
|
||||
},
|
||||
action: typeof action == 'function' || !action ? action : () => action,
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(onClose, timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function alert(option: AlertDialogOption = {}) {
|
||||
return show({
|
||||
...option,
|
||||
defaultStatus: 'ok',
|
||||
okButton: true,
|
||||
cancelButton: false,
|
||||
})
|
||||
}
|
||||
|
||||
async function confirm(option: ConfirmDialogOption = {}) {
|
||||
return show({
|
||||
...option,
|
||||
defaultStatus: 'cancel',
|
||||
okButton: true,
|
||||
cancelButton: true,
|
||||
})
|
||||
}
|
||||
return {
|
||||
confirm,
|
||||
alert,
|
||||
render,
|
||||
show,
|
||||
close,
|
||||
randomId,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Fn } from '@vueuse/core'
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import { inject, provide, ref, watch } from 'vue'
|
||||
|
||||
const IsOpenSymbol: InjectionKey<Ref<boolean>> = Symbol('is-open')
|
||||
|
||||
export function isShow() {
|
||||
return inject(IsOpenSymbol, ref(false))
|
||||
}
|
||||
|
||||
interface OnShowOption {
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export function onShow(fn: Fn, { immediate = false }: OnShowOption = {}) {
|
||||
return watch(
|
||||
isShow(),
|
||||
(val) => {
|
||||
if (val) {
|
||||
fn()
|
||||
}
|
||||
},
|
||||
{ immediate },
|
||||
)
|
||||
}
|
||||
|
||||
export function provideShow(ref: Ref<boolean>) {
|
||||
provide(IsOpenSymbol, ref)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
export type ClassType = string | string[]
|
||||
|
||||
export type BaseType = string | number | boolean | object | symbol | undefined
|
||||
|
||||
export const valuePropType = [String, Number, Object, Boolean, Array] as PropType<BaseType>
|
||||
|
||||
export const classPropType = [String, Array] as PropType<string | string[]>
|
||||
|
||||
export const labelPropType = [String, Function] as PropType<
|
||||
string | ((val?: BaseType) => string) | null
|
||||
>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { Primitive, type AsTag } from 'reka-ui'
|
||||
import { badgeVariants, type BadgeVariants } from './badgeVariants'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
as?: AsTag
|
||||
variant?: BadgeVariants['variant']
|
||||
}>(),
|
||||
{
|
||||
as: 'span',
|
||||
variant: 'default',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="cn(badgeVariants({ variant }), $attrs.class ?? '')" v-bind="$attrs">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex h-[18px] items-center rounded-none border px-[7px] text-[10px] font-normal leading-none',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-border bg-primary text-primary-foreground',
|
||||
secondary: 'border-border bg-secondary text-secondary-foreground',
|
||||
outline: 'border-border bg-transparent text-foreground',
|
||||
destructive: 'border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiBadge } from './UiBadge.vue'
|
||||
export { badgeVariants, type BadgeVariants } from './badgeVariants'
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { AsTag } from 'reka-ui'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { buttonVariants, type ButtonVariants } from './buttonVariants'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
as?: AsTag
|
||||
asChild?: boolean
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
}>(),
|
||||
{
|
||||
as: 'button',
|
||||
asChild: false,
|
||||
variant: 'primary',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'ui-button inline-flex items-center justify-center gap-2 whitespace-nowrap outline-none transition-[color,background,border-color,transform] disabled:pointer-events-none [&_[data-icon]]:pointer-events-none [&_[data-icon]]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'ui-button--primary',
|
||||
secondary: 'ui-button--secondary',
|
||||
outline: 'ui-button--outline',
|
||||
destructive: 'ui-button--destructive',
|
||||
ghost: 'ui-button--ghost',
|
||||
},
|
||||
size: {
|
||||
default: 'h-[30px] min-w-[110px] px-[14px] text-[13px]',
|
||||
sm: 'h-6 min-w-20 px-2.5 text-xs',
|
||||
lg: 'h-11 min-w-[168px] px-5 text-base',
|
||||
icon: 'size-[30px] min-w-[30px] p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiButton } from './UiButton.vue'
|
||||
export { buttonVariants, type ButtonVariants } from './buttonVariants'
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import {
|
||||
CheckboxIndicator,
|
||||
CheckboxRoot,
|
||||
type CheckboxRootEmits,
|
||||
type CheckboxRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _class, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'ui-checkbox peer inline-flex size-4 shrink-0 items-center justify-center border outline-none disabled:cursor-not-allowed',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<CheckboxIndicator class="ui-checkbox__indicator" />
|
||||
</CheckboxRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-checkbox {
|
||||
--check-edge-top: #635036;
|
||||
--check-edge-bottom: #3c2e1b;
|
||||
--check-top: #242424;
|
||||
--check-bottom: #0f0f0f;
|
||||
position: relative;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, var(--check-top), var(--check-bottom)) padding-box,
|
||||
linear-gradient(180deg, var(--check-edge-top), var(--check-edge-bottom)) border-box;
|
||||
box-shadow:
|
||||
0 0 0 1px rgb(0 0 0 / 30%),
|
||||
inset 0 1px 0 rgb(255 255 255 / 2.5%);
|
||||
color: var(--ui-primary-foreground);
|
||||
}
|
||||
|
||||
.ui-checkbox:hover:not(:disabled) {
|
||||
--check-edge-top: #bf9d6a;
|
||||
--check-edge-bottom: #947853;
|
||||
--check-top: #3a3a3a;
|
||||
--check-bottom: #181818;
|
||||
}
|
||||
|
||||
.ui-checkbox:focus-visible {
|
||||
outline: 1px solid var(--ui-ring);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.ui-checkbox:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.ui-checkbox__indicator {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
background: linear-gradient(180deg, #dfb142, #eac75f);
|
||||
clip-path: polygon(0 42%, 7% 34%, 39% 66%, 94% 4%, 100% 10%, 39% 82%);
|
||||
filter: drop-shadow(0 0 1px rgb(230 192 86 / 45%));
|
||||
animation: check-in 70ms ease-out;
|
||||
}
|
||||
|
||||
.ui-checkbox:disabled {
|
||||
--check-edge-top: #414141;
|
||||
--check-edge-bottom: #252525;
|
||||
--check-top: #1f1f1f;
|
||||
--check-bottom: #0d0d0d;
|
||||
}
|
||||
@keyframes check-in {
|
||||
from {
|
||||
transform: scale(0);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as UiCheckbox } from './UiCheckbox.vue'
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface UiDataTableColumn {
|
||||
key: string
|
||||
label: string
|
||||
sortable?: boolean
|
||||
width?: string
|
||||
}
|
||||
|
||||
export interface UiDataTableRow {
|
||||
id: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
columns: UiDataTableColumn[]
|
||||
rows: UiDataTableRow[]
|
||||
selectedId?: string
|
||||
sortKey?: string
|
||||
sortDirection?: 'ascending' | 'descending'
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
select: [row: UiDataTableRow]
|
||||
sort: [key: string]
|
||||
}>()
|
||||
|
||||
const gridTemplate = computed(() => props.columns.map((column) => column.width ?? '1fr').join(' '))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('ui-data-table', props.class)" role="table">
|
||||
<div
|
||||
class="ui-data-table__row ui-data-table__header"
|
||||
role="row"
|
||||
:style="{ gridTemplateColumns: gridTemplate }"
|
||||
>
|
||||
<template v-for="column in columns" :key="column.key">
|
||||
<button
|
||||
v-if="column.sortable"
|
||||
type="button"
|
||||
class="ui-data-table__sort"
|
||||
role="columnheader"
|
||||
:aria-sort="sortKey === column.key ? (sortDirection ?? 'ascending') : 'none'"
|
||||
@click="emits('sort', column.key)"
|
||||
>
|
||||
{{ column.label }}<i aria-hidden="true" />
|
||||
</button>
|
||||
<div v-else role="columnheader">{{ column.label }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-for="row in rows"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
role="row"
|
||||
:class="['ui-data-table__row', { 'is-selected': row.id === selectedId }]"
|
||||
:style="{ gridTemplateColumns: gridTemplate }"
|
||||
@click="emits('select', row)"
|
||||
>
|
||||
<span v-for="column in columns" :key="column.key" role="cell">
|
||||
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
|
||||
{{ row[column.key] }}
|
||||
</slot>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-data-table {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid #4a3c26;
|
||||
border-radius: 0;
|
||||
background: #030403;
|
||||
color: #d2c78f;
|
||||
}
|
||||
|
||||
.ui-data-table__row {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #2c2b26;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: 12px/1 inherit;
|
||||
text-align: left;
|
||||
min-width: 560px;
|
||||
}
|
||||
|
||||
button.ui-data-table__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.ui-data-table__row:focus-visible {
|
||||
outline: 1px solid var(--ui-ring);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
button.ui-data-table__row:nth-child(odd) {
|
||||
background: var(--ui-row-background);
|
||||
}
|
||||
button.ui-data-table__row:hover {
|
||||
background: linear-gradient(90deg, #132b43, #0b1824 58%, #070a0d);
|
||||
outline: 1px solid #3b91bd;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
button.ui-data-table__row.is-selected {
|
||||
background: linear-gradient(90deg, #352b11, #161207 60%, #090a08);
|
||||
outline: 1px solid #d6ad37;
|
||||
outline-offset: -1px;
|
||||
box-shadow: inset 0 0 0 1px #6d5218;
|
||||
}
|
||||
|
||||
.ui-data-table__row > * {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 9px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #292720;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-data-table__header {
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
column-gap: 1px;
|
||||
border-bottom: 0;
|
||||
background: #030403;
|
||||
}
|
||||
.ui-data-table__header > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #070806;
|
||||
background: var(--ui-table-header);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px #4a3c26,
|
||||
inset 0 1px 0 #65502a;
|
||||
color: var(--ui-table-header-foreground);
|
||||
font: 11px/1 inherit;
|
||||
}
|
||||
|
||||
.ui-data-table__sort {
|
||||
justify-content: space-between;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-data-table__sort:hover {
|
||||
filter: brightness(1.16);
|
||||
}
|
||||
.ui-data-table__sort:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.ui-data-table__sort i {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid currentColor;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.ui-data-table__sort[aria-sort='descending'] i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiDataTable } from './UiDataTable.vue'
|
||||
export type { UiDataTableColumn, UiDataTableRow } from './UiDataTable.vue'
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import {
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _class, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid w-[min(92vw,620px)] -translate-x-1/2 -translate-y-1/2 outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DialogContent>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-dialog-content {
|
||||
overflow: hidden;
|
||||
border: 1px solid #6c5a37;
|
||||
border-radius: 0;
|
||||
background: #050606;
|
||||
box-shadow:
|
||||
0 18px 38px rgb(0 0 0 / 80%),
|
||||
inset 0 0 0 1px #0e1418;
|
||||
color: var(--ui-surface-foreground);
|
||||
}
|
||||
</style>
|
||||