init
@@ -1 +0,0 @@
|
||||
DNF风格定制组件
|
||||
@@ -1,207 +0,0 @@
|
||||
<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>
|
||||
@@ -1,250 +0,0 @@
|
||||
<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>
|
||||
@@ -1,120 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 215 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 215 B |
@@ -1,277 +0,0 @@
|
||||
<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>
|
||||
@@ -1,73 +0,0 @@
|
||||
<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>
|
||||
@@ -1,226 +0,0 @@
|
||||
<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>
|
||||
@@ -1,41 +0,0 @@
|
||||
<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>
|
||||
@@ -1,201 +0,0 @@
|
||||
<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>
|
||||
@@ -1,257 +0,0 @@
|
||||
<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>
|
||||
@@ -1,40 +0,0 @@
|
||||
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
|
||||
@@ -1,69 +0,0 @@
|
||||
<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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<script lang="tsx">
|
||||
import { HiItem } from 'hoci'
|
||||
export default HiItem
|
||||
</script>
|
||||
@@ -1,95 +0,0 @@
|
||||
<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>
|
||||
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 557 B |
@@ -1,73 +0,0 @@
|
||||
<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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<script lang="tsx">
|
||||
import { HiItem } from 'hoci'
|
||||
export default HiItem
|
||||
</script>
|
||||
@@ -1,97 +0,0 @@
|
||||
<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>
|
||||
@@ -1,51 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 215 B |
@@ -1,330 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script lang="tsx">
|
||||
import { HiSelection } from 'hoci'
|
||||
|
||||
export default HiSelection
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export const CHECKBOX_GROUP: InjectionKey<boolean> = Symbol('checkbox-group')
|
||||
@@ -1,211 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
export const AciveSymbol = Symbol('[i-tabs]active')
|
||||
|
||||
export const AciveClassSymbol = Symbol('[i-tabs]active-class')
|
||||
|
||||
export const ChangeActiveSymbol = Symbol('[i-tabs]change-active')
|
||||
@@ -1,141 +0,0 @@
|
||||
<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>
|
||||
@@ -1,28 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 280 B |
|
Before Width: | Height: | Size: 322 B |
@@ -1,105 +0,0 @@
|
||||
<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>
|
||||
@@ -1,171 +0,0 @@
|
||||
<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>
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface TreeNode {
|
||||
label: string
|
||||
id?: string | number
|
||||
value?: any
|
||||
children?: TreeNode[]
|
||||
onSelect?: () => void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface TreeData {}
|
||||
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<div class="pos-relative w-300px h-full">
|
||||
<div class="avatar-list">
|
||||
<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)"
|
||||
>
|
||||
<div class="avatar-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
|
||||
{{ partName(part) }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -66,4 +62,26 @@ const partName = (part: string) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.avatar-list {
|
||||
position: relative;
|
||||
width: var(--ui-avatar-board-inline-size, 300px);
|
||||
height: var(--ui-avatar-board-block-size, 177px);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar-list__slot {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: var(--ui-avatar-slot-size, 26px);
|
||||
height: var(--ui-avatar-slot-size, 26px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--ui-component-stroke-size, 1px) solid rgb(122 122 122 / 50%);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div class="pos-relative w-248px h-full" :class="props.withBg ? 'emblem-bg' : ''">
|
||||
<div class="emblem-list" :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)"
|
||||
>
|
||||
<div class="emblem-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
|
||||
<img
|
||||
class="w-25px h-25px rounded-50%"
|
||||
class="emblem-list__rarity"
|
||||
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 != '白金'" />
|
||||
<img
|
||||
class="emblem-list__level"
|
||||
:style="lvStyle"
|
||||
:src="lvIcon(part)"
|
||||
v-if="part != '白金'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -102,7 +103,6 @@ const lvStyle = computed(() => {
|
||||
|
||||
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`,
|
||||
@@ -129,6 +129,35 @@ const choosePart = (part: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.emblem-list {
|
||||
position: relative;
|
||||
width: var(--ui-emblem-board-inline-size, 248px);
|
||||
height: var(--ui-emblem-board-block-size, 179px);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.emblem-list__slot {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: rgb(255 255 255 / 50%);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emblem-list__rarity {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.emblem-list__level {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.emblem-bg {
|
||||
background-image: url(../Images/head.png);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
<template>
|
||||
<template v-if="props.equipment">
|
||||
<div class="relative w-28px h-28px shrink-0" :class="{ inactive: props.inactive }">
|
||||
<div class="equipment-icon" :class="{ 'equipment-icon--inactive': props.inactive }">
|
||||
<div
|
||||
class="w-28px h-28px"
|
||||
class="equipment-icon__image"
|
||||
:style="{
|
||||
backgroundImage: `url(${getImageURL((useDisplayUrl ? props.equipment?.displayUrl : props.equipment?.imageUrl) || '')})`,
|
||||
}"
|
||||
>
|
||||
<img :src="getLocalImageURL(rarity)" />
|
||||
<img
|
||||
v-if="rarity"
|
||||
class="equipment-icon__rarity-frame"
|
||||
:src="getLocalImageURL(rarity)"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="box-border w-28px h-28px shrink-0 border-1px border-solid border-#7a7a7a/50"></div>
|
||||
<div class="equipment-icon equipment-icon--empty"></div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -47,38 +52,43 @@ 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");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.equipment-icon {
|
||||
position: relative;
|
||||
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
|
||||
height: var(--ui-equipment-icon-size, var(--ui-item-image-size));
|
||||
flex: 0 0 auto;
|
||||
|
||||
// .primeval_effect {
|
||||
// width: 28px;
|
||||
// height: 28px;
|
||||
// background-size: cover;
|
||||
// animation: primeval 2s infinite;
|
||||
// // 设置动画名称,持续时间,和循环次数
|
||||
// mix-blend-mode: color-dodge;
|
||||
// }
|
||||
&__image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
&::after {
|
||||
content: '';
|
||||
&__rarity-frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: #00000090;
|
||||
// filter: grayscale(100%);
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
box-sizing: border-box;
|
||||
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
|
||||
}
|
||||
|
||||
&--inactive::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
background: var(--ui-equipment-icon-inactive-overlay, var(--ui-item-slot-lock-overlay));
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
<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">
|
||||
<div class="equipment-info">
|
||||
<div class="equipment-info__panel">
|
||||
<div class="equipment-info__header">
|
||||
<div class="equipment-info__icon">
|
||||
<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 class="equipment-info__identity">
|
||||
<div :class="[rarity, 'equipment-info__name']">{{ props.equipment?.name }}</div>
|
||||
<div class="equipment-info__category">{{ 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="equipment-info__divider"></div>
|
||||
<div class="equipment-info__section equipment-info__section--end">
|
||||
<div :class="rarity">{{ props.equipment?.rarity }}</div>
|
||||
<div class="text-#7C6430">{{ props.equipment?.itemDetailType }}</div>
|
||||
<div class="equipment-info__type">{{ 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 class="equipment-info__divider"></div>
|
||||
<div class="equipment-info__section">
|
||||
<div class="equipment-info__attributes" 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 class="equipment-info__divider"></div>
|
||||
<div class="equipment-info__section">
|
||||
<div class="equipment-info__detail" 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 class="equipment-info__divider"></div>
|
||||
<div class="equipment-info__section">
|
||||
<div class="equipment-info__buffer-heading">辅助职业专属属性</div>
|
||||
<div class="equipment-info__buffer-detail" v-html="bufferDetail"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -93,7 +79,7 @@ const mainAttr = computed(() => {
|
||||
const detail = computed(() =>
|
||||
props.equipment?.detail?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
(match) => `<span class="equipment-info__markup">${match}</span>`,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -102,14 +88,114 @@ const bufferDetail = computed(() => {
|
||||
if (!detail || detail == '-') return undefined
|
||||
return props.equipment?.bufferDetail.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
(match) => `<span class="equipment-info__markup">${match}</span>`,
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divide {
|
||||
border-top: 1px solid rgba(243, 244, 246, 0.3);
|
||||
margin: 2px 0;
|
||||
.equipment-info {
|
||||
display: flow-root;
|
||||
width: fit-content;
|
||||
background: var(--ui-equipment-info-shell-background, var(--ui-surface));
|
||||
|
||||
&__panel {
|
||||
width: var(--ui-equipment-info-inline-size, 220px);
|
||||
height: auto;
|
||||
padding: var(--ui-equipment-info-padding, 5px);
|
||||
background-color: var(--ui-equipment-info-background, var(--ui-tooltip-background));
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
color: var(--ui-equipment-info-foreground, var(--ui-muted-foreground));
|
||||
font-size: var(--ui-control-font-size-compact);
|
||||
text-shadow:
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow));
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
height: var(--ui-equipment-info-row-block-size, 30px);
|
||||
align-items: center;
|
||||
margin-block: var(--ui-equipment-info-section-gap, 2px);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
|
||||
flex: 0 0 auto;
|
||||
margin-right: var(--ui-equipment-info-section-gap, 2px);
|
||||
}
|
||||
|
||||
&__identity,
|
||||
&__section {
|
||||
line-height: var(--ui-equipment-info-line-height, 15px);
|
||||
}
|
||||
|
||||
&__identity {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name,
|
||||
&__category {
|
||||
padding-left: var(--ui-equipment-info-padding, 5px);
|
||||
}
|
||||
|
||||
&__category {
|
||||
color: var(--ui-equipment-info-category, var(--ui-success));
|
||||
}
|
||||
|
||||
&__divider {
|
||||
margin-block: var(--ui-equipment-info-section-gap, 2px);
|
||||
border-top: var(--ui-component-stroke-size) solid
|
||||
var(--ui-equipment-info-divider, var(--ui-border));
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-block: var(--ui-equipment-info-section-margin-block, 1px);
|
||||
white-space: pre-wrap;
|
||||
|
||||
&--end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&__type {
|
||||
color: var(--ui-equipment-info-type, var(--ui-secondary-foreground));
|
||||
}
|
||||
|
||||
&__attributes {
|
||||
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
|
||||
color: var(--ui-equipment-info-attribute, var(--ui-secondary-foreground));
|
||||
}
|
||||
|
||||
&__detail,
|
||||
&__buffer-detail {
|
||||
color: var(--ui-equipment-info-detail, var(--ui-tooltip-foreground));
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
|
||||
}
|
||||
|
||||
&__buffer-heading {
|
||||
margin-bottom: var(--ui-equipment-info-buffer-gap, 10px);
|
||||
padding-top: var(--ui-equipment-info-detail-padding-block, 4px);
|
||||
color: var(--ui-equipment-info-buffer-heading, var(--ui-warning));
|
||||
}
|
||||
|
||||
&__buffer-detail {
|
||||
padding-bottom: var(--ui-equipment-info-buffer-gap, 10px);
|
||||
}
|
||||
|
||||
:deep(.equipment-info__markup) {
|
||||
color: var(--ui-equipment-info-markup, var(--ui-success));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="pos-relative w-264px h-full"
|
||||
class="equipment-list"
|
||||
: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)"
|
||||
>
|
||||
<div class="equipment-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
|
||||
<template v-if="detail?.[part]?.id">
|
||||
<EquipmentIcon :equipment="equ(detail[part].id)" />
|
||||
</template>
|
||||
@@ -16,14 +12,10 @@
|
||||
{{ 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"
|
||||
class="equipment-list__slot equipment-list__slot--subweapon"
|
||||
@click="choosePart('副武器')"
|
||||
>
|
||||
<template v-if="detail?.['副武器']?.id">
|
||||
@@ -31,12 +23,7 @@
|
||||
</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"
|
||||
/>
|
||||
<img v-if="props.avatar" :src="props.avatar" alt="Avatar" class="equipment-list__avatar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -108,13 +95,10 @@ const infoStyle = (part: string) => {
|
||||
x += (index % 2) * 39
|
||||
y += Math.floor(index / 2) * 34
|
||||
|
||||
const style = {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
zIndex: 6,
|
||||
// fontWeight: 900,
|
||||
return {
|
||||
'--ui-equipment-slot-left': `${x}px`,
|
||||
'--ui-equipment-slot-top': `${y}px`,
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// const fusionStyle = (part: string) => {
|
||||
@@ -152,4 +136,50 @@ const partName = (part: string) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.equipment-list {
|
||||
position: relative;
|
||||
width: var(--ui-equipment-board-inline-size, 264px);
|
||||
/* Board contract: absolute-positioned slots need a definite block size in every host. */
|
||||
height: var(--ui-equipment-board-block-size, 177px);
|
||||
flex: 0 0 auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--ui-equipment-board-inline-size, 264px) auto;
|
||||
|
||||
&__slot {
|
||||
position: absolute;
|
||||
z-index: var(--ui-equipment-slot-z-index, 6);
|
||||
left: var(--ui-equipment-slot-left);
|
||||
top: var(--ui-equipment-slot-top);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: var(--ui-equipment-slot-size, 26px);
|
||||
height: var(--ui-equipment-slot-size, 26px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
|
||||
color: var(--ui-equipment-slot-foreground, var(--ui-muted-foreground));
|
||||
font-size: var(--ui-equipment-preview-slot-font-size, 12px);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
|
||||
&--subweapon {
|
||||
--ui-equipment-slot-left: var(--ui-equipment-subweapon-left, 150px);
|
||||
--ui-equipment-slot-top: var(--ui-equipment-subweapon-top, 5px);
|
||||
|
||||
z-index: calc(var(--ui-equipment-slot-z-index, 6) + 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(0px - var(--ui-equipment-avatar-bottom-offset, 10px));
|
||||
width: var(--ui-equipment-avatar-inline-size, 70%);
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="pos-relative w-264px h-full"
|
||||
class="equipment-oath"
|
||||
: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)"
|
||||
>
|
||||
<div class="equipment-oath__slot" :style="infoStyle(part)" @click="choosePart(part)">
|
||||
<template v-if="detail?.[part]?.id">
|
||||
<EquipmentIcon :equipment="equ(detail[part].id, part)" useDisplayUrl />
|
||||
</template>
|
||||
@@ -16,16 +12,7 @@
|
||||
{{ 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>
|
||||
|
||||
@@ -158,20 +145,12 @@ const positionSimple = [
|
||||
]
|
||||
|
||||
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 coordinates = (props.withBg ? position : positionSimple)[Number.parseInt(part)]
|
||||
|
||||
return {
|
||||
'--ui-equipment-slot-left': `${coordinates.x}px`,
|
||||
'--ui-equipment-slot-top': `${coordinates.y}px`,
|
||||
}
|
||||
}
|
||||
|
||||
const choosePart = (part: string) => {
|
||||
@@ -179,4 +158,32 @@ const choosePart = (part: string) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.equipment-oath {
|
||||
position: relative;
|
||||
width: var(--ui-equipment-board-inline-size, 264px);
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--ui-equipment-board-inline-size, 264px) auto;
|
||||
|
||||
&__slot {
|
||||
position: absolute;
|
||||
z-index: var(--ui-equipment-slot-z-index, 6);
|
||||
left: var(--ui-equipment-slot-left);
|
||||
top: var(--ui-equipment-slot-top);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: var(--ui-equipment-slot-size, 26px);
|
||||
height: var(--ui-equipment-slot-size, 26px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
|
||||
color: var(--ui-equipment-slot-foreground, var(--ui-muted-foreground));
|
||||
font-size: var(--ui-equipment-preview-slot-font-size, 12px);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<template>
|
||||
<calc-cascader
|
||||
class="!h-5 flex-1"
|
||||
<UiTreeSelect
|
||||
class="part-select"
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
select-parents
|
||||
placeholder="请输入名称搜索"
|
||||
></calc-cascader>
|
||||
label="装备部位"
|
||||
@select="selectPart"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TreeNode } from '@/components/calc/tree/types'
|
||||
import { UiTreeSelect, type UiTreeSelectItem } from '@/components/ui/tree-select'
|
||||
import { useInfoStore } from '@/stores'
|
||||
import { syncRef, useVModel } from '@vueuse/core'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
partList: {
|
||||
@@ -27,7 +30,9 @@ const special_parts = ['辅助装备', '魔法石', '耳环']
|
||||
|
||||
const else_parts = ['称号', '宠物']
|
||||
|
||||
const emit = defineEmits([])
|
||||
const emit = defineEmits<{
|
||||
'update:partList': [value: string[]]
|
||||
}>()
|
||||
|
||||
const choose_part = useVModel(props, 'partList', emit, { passive: true })
|
||||
|
||||
@@ -35,95 +40,92 @@ const selected = ref('')
|
||||
|
||||
const infoStore = useInfoStore()
|
||||
|
||||
const items = computed<TreeNode[]>(() => {
|
||||
const partGroups: Record<string, string[]> = {
|
||||
防具: armor_parts,
|
||||
首饰: jewel_parts,
|
||||
特殊装备: special_parts,
|
||||
其它: else_parts,
|
||||
}
|
||||
|
||||
function selectPart(item: UiTreeSelectItem) {
|
||||
if (!item.value) {
|
||||
choose_part.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (item.value === '武器') {
|
||||
choose_part.value = infoStore.infos?.weapons ?? []
|
||||
return
|
||||
}
|
||||
|
||||
choose_part.value = partGroups[item.value] ? [...partGroups[item.value]] : [item.value]
|
||||
}
|
||||
|
||||
const items = computed<UiTreeSelectItem[]>(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.part-select {
|
||||
--ui-control-height-default: var(--ui-control-height-compact);
|
||||
|
||||
min-width: min(100%, var(--ui-tree-select-min-inline-size));
|
||||
flex: 1 1 var(--ui-tree-select-min-inline-size);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
<template>
|
||||
<div class="flex gap-8px">
|
||||
<template v-for="(rarity, _) in rarities" :key="_">
|
||||
<div class="equipment-rarity-select">
|
||||
<template v-for="rarity in rarities" :key="rarity.value">
|
||||
<div
|
||||
class="w-14px h-14px shadow-[0px_0px_3px_#ffffff_inset,0px_0px_1px_#ffffff40]"
|
||||
:style="{ background: rarity.background }"
|
||||
class="equipment-rarity-select__swatch"
|
||||
:class="rarity.className"
|
||||
@click="choose(rarity.value)"
|
||||
>
|
||||
<div class="w-14px h-14px" :class="{ active: value === rarity.value }"></div>
|
||||
<div
|
||||
class="equipment-rarity-select__mark"
|
||||
:class="{ 'equipment-rarity-select__mark--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',
|
||||
className: 'equipment-rarity-select__swatch--rare',
|
||||
},
|
||||
{
|
||||
value: '神器',
|
||||
background: '#ef28c1',
|
||||
className: 'equipment-rarity-select__swatch--artifact',
|
||||
},
|
||||
{
|
||||
value: '传说',
|
||||
background: '#fc8711',
|
||||
className: 'equipment-rarity-select__swatch--legend',
|
||||
},
|
||||
{
|
||||
value: '史诗',
|
||||
background: '#fcb600',
|
||||
className: 'equipment-rarity-select__swatch--epic',
|
||||
},
|
||||
{
|
||||
value: '太初',
|
||||
background: 'linear-gradient(to bottom, #58fa84 20%, #58decc 40%, #58c6ff 60%)',
|
||||
className: 'equipment-rarity-select__swatch--primeval',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -50,8 +51,63 @@ const choose = (rarity: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active {
|
||||
background: url('@/assets/img/control/item_checked.png') no-repeat center;
|
||||
background-size: 12px 10px;
|
||||
.equipment-rarity-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ui-equipment-rarity-swatch-gap, 8px);
|
||||
|
||||
&__swatch,
|
||||
&__mark {
|
||||
width: var(--ui-equipment-rarity-swatch-size, 14px);
|
||||
height: var(--ui-equipment-rarity-swatch-size, 14px);
|
||||
}
|
||||
|
||||
&__swatch {
|
||||
flex: 0 0 auto;
|
||||
box-shadow:
|
||||
inset 0 0 3px var(--ui-equipment-rarity-swatch-highlight, var(--ui-popover-foreground)),
|
||||
0 0 1px
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--ui-equipment-rarity-swatch-highlight, var(--ui-popover-foreground)) 25%,
|
||||
transparent
|
||||
);
|
||||
|
||||
&--rare {
|
||||
background: var(--dcalc-quality-rare);
|
||||
}
|
||||
|
||||
&--artifact {
|
||||
background: var(--dcalc-quality-artifact);
|
||||
}
|
||||
|
||||
&--legend {
|
||||
background: var(--dcalc-quality-legend);
|
||||
}
|
||||
|
||||
&--epic {
|
||||
background: var(--dcalc-quality-epic);
|
||||
}
|
||||
|
||||
&--primeval {
|
||||
background: var(
|
||||
--ui-equipment-rarity-primeval-background,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
var(--dcalc-quality-primeval) 20%,
|
||||
color-mix(in srgb, var(--dcalc-quality-primeval) 55%, var(--ui-info)) 40%,
|
||||
var(--ui-info) 60%
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__mark--active {
|
||||
background-image: url('@/assets/img/control/item_checked.png');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--ui-equipment-rarity-mark-inline-size, 12px)
|
||||
var(--ui-equipment-rarity-mark-block-size, 10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
<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="equipment-suit-info">
|
||||
<div class="equipment-suit-info__panel">
|
||||
<!-- 装备图标 装备名称 -->
|
||||
<div class="flex w-full items-center justify-center gap-5px">
|
||||
<div>
|
||||
<div class="equipment-suit-info__header">
|
||||
<div class="equipment-suit-info__icon">
|
||||
<img
|
||||
class="equipment-suit-info__icon-image"
|
||||
:src="getImageURL('equipment' + props.suit?.imageUrl.replace('/suit/', '/suit/icon/'))"
|
||||
alt="装备图标"
|
||||
/>
|
||||
@@ -23,25 +13,22 @@
|
||||
<div :class="rarity">{{ props.suit?.name }}</div>
|
||||
<div :class="rarity">{{ props.suit?.point }}点</div>
|
||||
</div>
|
||||
<div class="divide"></div>
|
||||
<div class="equipment-suit-info__divider"></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 class="equipment-suit-info__section">
|
||||
<div class="equipment-suit-info__attributes" 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 class="equipment-suit-info__divider"></div>
|
||||
<div class="equipment-suit-info__section">
|
||||
<div class="equipment-suit-info__detail" 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 class="equipment-suit-info__divider"></div>
|
||||
<div class="equipment-suit-info__section">
|
||||
<div class="equipment-suit-info__buffer-heading">辅助职业专属属性</div>
|
||||
<div class="equipment-suit-info__buffer-detail" v-html="bufferDetail"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -76,7 +63,7 @@ const mainAttr = computed(() => {
|
||||
const detail = computed(() =>
|
||||
props.suit?.value?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
(match) => `<span class="equipment-suit-info__markup">${match}</span>`,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -85,14 +72,97 @@ const bufferDetail = computed(() => {
|
||||
if (!detail || detail == '-') return undefined
|
||||
return props.suit?.bufferValue?.replace(
|
||||
/\[(?![^\]]*技能)[^[\]]+\]/g,
|
||||
(match) => `<span style="color:#3d9147">${match}</span>`,
|
||||
(match) => `<span class="equipment-suit-info__markup">${match}</span>`,
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divide {
|
||||
border-top: 1px solid rgba(243, 244, 246, 0.3);
|
||||
margin: 2px 0;
|
||||
.equipment-suit-info {
|
||||
display: flow-root;
|
||||
width: fit-content;
|
||||
background: var(--ui-equipment-info-shell-background, var(--ui-surface));
|
||||
|
||||
&__panel {
|
||||
width: var(--ui-equipment-info-inline-size, 220px);
|
||||
height: auto;
|
||||
padding: var(--ui-equipment-info-padding, 5px);
|
||||
background-color: var(--ui-equipment-info-background, var(--ui-tooltip-background));
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
color: var(--ui-equipment-info-foreground, var(--ui-muted-foreground));
|
||||
font-size: var(--ui-control-font-size-compact);
|
||||
text-shadow:
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
|
||||
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow));
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
min-height: var(--ui-equipment-info-row-block-size, 30px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ui-equipment-info-padding, 5px);
|
||||
line-height: var(--ui-equipment-info-line-height, 15px);
|
||||
}
|
||||
|
||||
&__icon,
|
||||
&__icon-image {
|
||||
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
|
||||
height: var(--ui-equipment-icon-size, var(--ui-item-image-size));
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__icon-image {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
margin-block: var(--ui-equipment-info-section-gap, 2px);
|
||||
border-top: var(--ui-component-stroke-size) solid
|
||||
var(--ui-equipment-info-divider, var(--ui-border));
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-block: var(--ui-equipment-info-section-margin-block, 1px);
|
||||
line-height: var(--ui-equipment-info-line-height, 15px);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&__attributes {
|
||||
color: var(--ui-equipment-info-attribute, var(--ui-secondary-foreground));
|
||||
}
|
||||
|
||||
&__detail,
|
||||
&__buffer-detail {
|
||||
color: var(--ui-equipment-info-detail, var(--ui-tooltip-foreground));
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
|
||||
}
|
||||
|
||||
&__buffer-heading {
|
||||
margin-bottom: var(--ui-equipment-info-buffer-gap, 10px);
|
||||
padding-top: var(--ui-equipment-info-detail-padding-block, 4px);
|
||||
color: var(--ui-equipment-info-buffer-heading, var(--ui-warning));
|
||||
}
|
||||
|
||||
&__buffer-detail {
|
||||
padding-bottom: var(--ui-equipment-info-buffer-gap, 10px);
|
||||
}
|
||||
|
||||
:deep(.equipment-suit-info__markup) {
|
||||
color: var(--ui-equipment-info-markup, var(--ui-success));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { h, reactive, renderList } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import ActionDialog from '@/components/calc/action-dialog/index.vue'
|
||||
import { UiActionDialog } from '@/components/ui/dialog'
|
||||
import type { JSX } from 'vue/jsx-runtime'
|
||||
|
||||
export type ConfirmDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'>
|
||||
@@ -87,14 +87,14 @@ export const useDialog = createSharedComposable(() => {
|
||||
return id
|
||||
}
|
||||
|
||||
function close(id: DialogID, status = 'cancel', duration = 400) {
|
||||
function close(id: DialogID, status: DialogResult['status'] = 'cancel', duration = 400) {
|
||||
const dialog = dialogs.get(id)
|
||||
if (!dialog) {
|
||||
return
|
||||
}
|
||||
dialog.resolve({
|
||||
id,
|
||||
status: 'ok',
|
||||
status,
|
||||
get isOk() {
|
||||
return status == 'ok'
|
||||
},
|
||||
@@ -119,6 +119,10 @@ export const useDialog = createSharedComposable(() => {
|
||||
}
|
||||
|
||||
async function show(option: ShowDialogOption = {}) {
|
||||
const activeElement = typeof document === 'undefined' ? null : document.activeElement
|
||||
const returnFocusElement =
|
||||
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
|
||||
|
||||
return new Promise<DialogResult>((resolve) => {
|
||||
const {
|
||||
id = randomId(),
|
||||
@@ -145,7 +149,7 @@ export const useDialog = createSharedComposable(() => {
|
||||
resolve,
|
||||
render(visible = true) {
|
||||
return h(
|
||||
ActionDialog,
|
||||
UiActionDialog,
|
||||
{
|
||||
header,
|
||||
okButton,
|
||||
@@ -154,6 +158,7 @@ export const useDialog = createSharedComposable(() => {
|
||||
visible,
|
||||
onClose,
|
||||
mask,
|
||||
returnFocusElement,
|
||||
scale,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonHTMLAttributes } from 'vue'
|
||||
import type { AsTag } from 'reka-ui'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { buttonVariants, type ButtonVariants } from './buttonVariants'
|
||||
@@ -12,22 +13,34 @@ withDefaults(
|
||||
asChild?: boolean
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
disabled?: boolean
|
||||
type?: ButtonHTMLAttributes['type']
|
||||
title?: string
|
||||
}>(),
|
||||
{
|
||||
as: 'button',
|
||||
asChild: false,
|
||||
variant: 'primary',
|
||||
size: 'default',
|
||||
type: 'button',
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:disabled="disabled"
|
||||
:type="type"
|
||||
:title="title"
|
||||
:class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')"
|
||||
v-bind="$attrs"
|
||||
@click="emits('click', $event)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed, nextTick, ref, useId, watch } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface UiCommandInputOption {
|
||||
value: string
|
||||
label: string
|
||||
prefix?: string
|
||||
tone?: 'default' | 'success' | 'warning' | 'danger'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
suggestions?: UiCommandInputOption[]
|
||||
prefix?: string
|
||||
prefixTone?: UiCommandInputOption['tone']
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
openOnFocus?: boolean
|
||||
filterSuggestions?: boolean
|
||||
label?: string
|
||||
}>(),
|
||||
{
|
||||
suggestions: () => [],
|
||||
prefix: '',
|
||||
prefixTone: 'default',
|
||||
openOnFocus: false,
|
||||
filterSuggestions: false,
|
||||
label: '命令输入',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [option: UiCommandInputOption]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>({ default: '' })
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const root = ref<HTMLElement>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const highlightedIndex = ref(-1)
|
||||
const listboxId = `ui-command-input-${useId()}`
|
||||
|
||||
const visibleSuggestions = computed(() => {
|
||||
if (!props.filterSuggestions || !model.value.trim()) return props.suggestions
|
||||
|
||||
const query = model.value.trim().toLocaleLowerCase()
|
||||
return props.suggestions.filter((option) =>
|
||||
[option.value, option.label, option.prefix]
|
||||
.filter(Boolean)
|
||||
.some((text) => text!.toLocaleLowerCase().includes(query)),
|
||||
)
|
||||
})
|
||||
|
||||
const listVisible = computed(
|
||||
() => open.value && !props.disabled && visibleSuggestions.value.length > 0,
|
||||
)
|
||||
|
||||
const activeDescendant = computed(() => {
|
||||
if (!listVisible.value || highlightedIndex.value < 0) return undefined
|
||||
return `${listboxId}-option-${highlightedIndex.value}`
|
||||
})
|
||||
|
||||
function firstEnabledIndex(options = visibleSuggestions.value) {
|
||||
return options.findIndex((option) => !option.disabled)
|
||||
}
|
||||
|
||||
function lastEnabledIndex(options = visibleSuggestions.value) {
|
||||
return options.findLastIndex((option) => !option.disabled)
|
||||
}
|
||||
|
||||
function moveHighlight(direction: 1 | -1) {
|
||||
const options = visibleSuggestions.value
|
||||
if (!options.length) return
|
||||
|
||||
let index = highlightedIndex.value
|
||||
for (let count = 0; count < options.length; count += 1) {
|
||||
index = (index + direction + options.length) % options.length
|
||||
if (!options[index]?.disabled) {
|
||||
highlightedIndex.value = index
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSuggestions() {
|
||||
if (props.disabled || props.readonly || !visibleSuggestions.value.length) return
|
||||
open.value = true
|
||||
highlightedIndex.value = firstEnabledIndex()
|
||||
}
|
||||
|
||||
function choose(option: UiCommandInputOption) {
|
||||
if (props.disabled || props.readonly || option.disabled) return
|
||||
|
||||
model.value = option.value
|
||||
open.value = false
|
||||
emit('select', option)
|
||||
void nextTick(() => input.value?.focus())
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
void nextTick(showSuggestions)
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
if (props.openOnFocus) showSuggestions()
|
||||
}
|
||||
|
||||
function handleFocusOut(event: FocusEvent) {
|
||||
const nextTarget = event.relatedTarget
|
||||
if (!(nextTarget instanceof Node) || !root.value?.contains(nextTarget)) open.value = false
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (props.disabled || props.readonly) return
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (!listVisible.value) showSuggestions()
|
||||
else moveHighlight(1)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (!listVisible.value) {
|
||||
showSuggestions()
|
||||
highlightedIndex.value = lastEnabledIndex()
|
||||
} else moveHighlight(-1)
|
||||
break
|
||||
case 'Home':
|
||||
if (!listVisible.value) return
|
||||
event.preventDefault()
|
||||
highlightedIndex.value = firstEnabledIndex()
|
||||
break
|
||||
case 'End':
|
||||
if (!listVisible.value) return
|
||||
event.preventDefault()
|
||||
highlightedIndex.value = lastEnabledIndex()
|
||||
break
|
||||
case 'Enter': {
|
||||
if (!listVisible.value) return
|
||||
event.preventDefault()
|
||||
const option = visibleSuggestions.value[highlightedIndex.value]
|
||||
if (option) choose(option)
|
||||
break
|
||||
}
|
||||
case 'Escape':
|
||||
if (!listVisible.value) return
|
||||
event.preventDefault()
|
||||
open.value = false
|
||||
break
|
||||
case 'Tab':
|
||||
open.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visibleSuggestions.value,
|
||||
(options) => {
|
||||
if (!options.length) {
|
||||
open.value = false
|
||||
highlightedIndex.value = -1
|
||||
return
|
||||
}
|
||||
|
||||
if (highlightedIndex.value < 0 || options[highlightedIndex.value]?.disabled) {
|
||||
highlightedIndex.value = firstEnabledIndex(options)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
defineExpose({ focus: () => input.value?.focus() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
:class="cn('ui-command-input', disabled && 'is-disabled', props.class)"
|
||||
@focusout="handleFocusOut"
|
||||
>
|
||||
<div class="ui-command-input__field" :class="{ 'is-open': listVisible }">
|
||||
<span
|
||||
v-if="prefix || $slots.prefix"
|
||||
class="ui-command-input__prefix"
|
||||
:class="`is-${prefixTone}`"
|
||||
>
|
||||
<slot name="prefix">{{ prefix }}</slot>
|
||||
</span>
|
||||
<input
|
||||
ref="input"
|
||||
v-model="model"
|
||||
class="ui-command-input__input"
|
||||
type="text"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="list"
|
||||
:aria-label="label"
|
||||
:aria-controls="listboxId"
|
||||
:aria-expanded="listVisible"
|
||||
:aria-activedescendant="activeDescendant"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="listVisible" :id="listboxId" class="ui-command-input__suggestions" role="listbox">
|
||||
<button
|
||||
v-for="(option, index) in visibleSuggestions"
|
||||
:id="`${listboxId}-option-${index}`"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="option"
|
||||
class="ui-command-input__option"
|
||||
:class="{ 'is-highlighted': highlightedIndex === index }"
|
||||
:aria-selected="model === option.value"
|
||||
:disabled="option.disabled"
|
||||
@mouseenter="highlightedIndex = index"
|
||||
@click="choose(option)"
|
||||
>
|
||||
<span
|
||||
v-if="option.prefix ?? prefix"
|
||||
class="ui-command-input__option-prefix"
|
||||
:class="`is-${option.tone ?? prefixTone}`"
|
||||
>
|
||||
{{ option.prefix ?? prefix }}
|
||||
</span>
|
||||
<span class="ui-command-input__option-label">{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ui-command-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-command-input__field {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--ui-control-height-default);
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-input);
|
||||
background: var(--ui-field-background);
|
||||
box-shadow: inset 0 0 0
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-inset);
|
||||
|
||||
&:focus-within,
|
||||
&.is-open {
|
||||
border-color: var(--ui-ring);
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-focus-inset),
|
||||
0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-focus-edge);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-command-input__prefix,
|
||||
.ui-command-input__option-prefix {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-color: var(--ui-command-prefix-border, var(--ui-disclosure-border));
|
||||
background: var(--ui-command-prefix-background, var(--ui-settings-row-alt));
|
||||
color: var(--ui-command-prefix-foreground, var(--ui-settings-section-foreground));
|
||||
line-height: var(--ui-control-line-height);
|
||||
|
||||
&.is-success {
|
||||
color: var(--ui-command-prefix-success, var(--ui-success));
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
color: var(--ui-warning);
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
color: var(--ui-destructive-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-command-input__prefix {
|
||||
align-self: stretch;
|
||||
min-width: var(
|
||||
--ui-command-prefix-min-inline-size,
|
||||
calc(var(--ui-control-height-default) + var(--ui-control-padding-inline-compact) * 2)
|
||||
);
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
border-width: 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) 0 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.ui-command-input__input {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
padding: 0 var(--ui-field-padding-inline);
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--ui-foreground);
|
||||
font: inherit;
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
line-height: var(--ui-control-line-height);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ui-muted-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-command-input.is-disabled {
|
||||
.ui-command-input__field {
|
||||
border-color: var(--ui-control-disabled-border);
|
||||
background: var(--ui-field-disabled-background);
|
||||
}
|
||||
|
||||
.ui-command-input__prefix,
|
||||
.ui-command-input__input {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-command-input__suggestions {
|
||||
position: absolute;
|
||||
z-index: var(--ui-popup-z-index, var(--ui-settings-dropdown-z-index));
|
||||
top: calc(100% + var(--ui-autocomplete-trigger-gap));
|
||||
right: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: var(--ui-autocomplete-max-block-size);
|
||||
overflow-y: auto;
|
||||
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-popup-border);
|
||||
background: var(--ui-popup-background);
|
||||
box-shadow: var(--ui-panel-shadow);
|
||||
}
|
||||
|
||||
.ui-command-input__option {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--ui-option-height);
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
padding: 0 var(--ui-field-padding-inline);
|
||||
border: 0;
|
||||
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-option-divider);
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--ui-primary-foreground);
|
||||
font: inherit;
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&.is-highlighted:not(:disabled) {
|
||||
outline: var(
|
||||
--ui-focus-outline-size,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
solid var(--ui-ring);
|
||||
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
|
||||
background: var(--ui-option-highlight-background);
|
||||
color: var(--ui-option-highlight-foreground);
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background: var(--ui-disclosure-row-selected);
|
||||
color: var(--ui-selected);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-command-input__option-prefix {
|
||||
min-width: var(
|
||||
--ui-command-option-prefix-min-inline-size,
|
||||
calc(var(--ui-control-height-default) + var(--ui-control-padding-inline-compact))
|
||||
);
|
||||
height: var(--ui-badge-height);
|
||||
padding-inline: var(--ui-badge-padding-inline);
|
||||
border-width: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4));
|
||||
border-style: solid;
|
||||
font-size: var(--ui-badge-font-size);
|
||||
}
|
||||
|
||||
.ui-command-input__option-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiCommandInput } from './UiCommandInput.vue'
|
||||
export type { UiCommandInputOption } from './UiCommandInput.vue'
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { CSSProperties, HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface UiDataTableColumn {
|
||||
@@ -7,21 +7,38 @@ export interface UiDataTableColumn {
|
||||
label: string
|
||||
sortable?: boolean
|
||||
width?: string
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}
|
||||
|
||||
export interface UiDataTableRow {
|
||||
id: string
|
||||
[key: string]: string
|
||||
kind?: 'data' | 'note'
|
||||
selectable?: boolean
|
||||
disabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
columns: UiDataTableColumn[]
|
||||
rows: UiDataTableRow[]
|
||||
selectedId?: string
|
||||
sortKey?: string
|
||||
sortDirection?: 'ascending' | 'descending'
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
columns: UiDataTableColumn[]
|
||||
rows: UiDataTableRow[]
|
||||
selectedId?: string
|
||||
sortKey?: string
|
||||
sortDirection?: 'ascending' | 'descending'
|
||||
density?: 'default' | 'compact'
|
||||
selectionTone?: 'gold' | 'blue'
|
||||
selectable?: boolean
|
||||
emptyText?: string
|
||||
maxBodyHeight?: string
|
||||
}>(),
|
||||
{
|
||||
density: 'default',
|
||||
selectionTone: 'gold',
|
||||
selectable: true,
|
||||
emptyText: '暂无数据',
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
select: [row: UiDataTableRow]
|
||||
@@ -29,10 +46,32 @@ const emits = defineEmits<{
|
||||
}>()
|
||||
|
||||
const gridTemplate = computed(() => props.columns.map((column) => column.width ?? '1fr').join(' '))
|
||||
const bodyStyle = computed<CSSProperties | undefined>(() =>
|
||||
props.maxBodyHeight ? { '--ui-table-body-max-block-size': props.maxBodyHeight } : undefined,
|
||||
)
|
||||
|
||||
function isSelectable(row: UiDataTableRow) {
|
||||
return props.selectable && row.kind !== 'note' && row.selectable !== false && !row.disabled
|
||||
}
|
||||
|
||||
function selectRow(row: UiDataTableRow) {
|
||||
if (isSelectable(row)) emits('select', row)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('ui-data-table', props.class)" role="table">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'ui-data-table',
|
||||
`ui-data-table--${density}`,
|
||||
`ui-data-table--selection-${selectionTone}`,
|
||||
maxBodyHeight && 'has-constrained-body',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
role="table"
|
||||
>
|
||||
<div
|
||||
class="ui-data-table__row ui-data-table__header"
|
||||
role="row"
|
||||
@@ -49,25 +88,76 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
|
||||
>
|
||||
{{ column.label }}<i aria-hidden="true" />
|
||||
</button>
|
||||
<div v-else role="columnheader">{{ column.label }}</div>
|
||||
<div v-else role="columnheader" :class="`is-align-${column.align ?? 'start'}`">
|
||||
{{ column.label }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-for="row in rows"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
<div class="ui-data-table__body" role="rowgroup" :style="bodyStyle">
|
||||
<template v-if="rows.length">
|
||||
<template v-for="row in rows" :key="row.id">
|
||||
<div
|
||||
v-if="row.kind === 'note'"
|
||||
class="ui-data-table__note"
|
||||
role="row"
|
||||
:aria-disabled="row.disabled || undefined"
|
||||
>
|
||||
<slot name="note" :row="row">{{ row.label ?? row.note ?? '' }}</slot>
|
||||
</div>
|
||||
<component
|
||||
:is="isSelectable(row) ? 'button' : 'div'"
|
||||
v-else
|
||||
:type="isSelectable(row) ? 'button' : undefined"
|
||||
role="row"
|
||||
:tabindex="isSelectable(row) ? 0 : undefined"
|
||||
:aria-disabled="row.disabled || undefined"
|
||||
:class="[
|
||||
'ui-data-table__row',
|
||||
'ui-data-table__data-row',
|
||||
{
|
||||
'is-selectable': isSelectable(row),
|
||||
'is-selected': row.id === selectedId,
|
||||
'is-disabled': row.disabled,
|
||||
},
|
||||
]"
|
||||
:style="{ gridTemplateColumns: gridTemplate }"
|
||||
@click="selectRow(row)"
|
||||
@keydown.enter.prevent="selectRow(row)"
|
||||
@keydown.space.prevent="selectRow(row)"
|
||||
>
|
||||
<span
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
role="cell"
|
||||
:class="`is-align-${column.align ?? 'start'}`"
|
||||
>
|
||||
<slot :name="`cell-${column.key}`" :row="row" :value="String(row[column.key] ?? '')">
|
||||
{{ String(row[column.key] ?? '') }}
|
||||
</slot>
|
||||
</span>
|
||||
</component>
|
||||
<div v-if="$slots['after-row']" class="ui-data-table__after-row" role="row">
|
||||
<slot name="after-row" :row="row" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else class="ui-data-table__empty" role="row">
|
||||
<slot name="empty">{{ emptyText }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="$slots.summary"
|
||||
class="ui-data-table__row ui-data-table__summary"
|
||||
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>
|
||||
<slot name="summary" :columns="columns" />
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="ui-data-table__footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,6 +171,17 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
|
||||
color: var(--ui-table-foreground);
|
||||
}
|
||||
|
||||
.ui-data-table__body {
|
||||
min-width: var(--ui-table-scroll-inline-size);
|
||||
}
|
||||
|
||||
.ui-data-table.has-constrained-body .ui-data-table__body {
|
||||
max-height: var(--ui-table-body-max-block-size);
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--ui-scrollbar-thumb-border) var(--ui-scrollbar-background);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.ui-data-table__row {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@@ -97,26 +198,31 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
|
||||
min-width: var(--ui-table-scroll-inline-size);
|
||||
}
|
||||
|
||||
button.ui-data-table__row {
|
||||
.ui-data-table__data-row.is-selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.ui-data-table__row:focus-visible {
|
||||
.ui-data-table__data-row.is-selectable:focus-visible {
|
||||
outline: 1px solid var(--ui-ring);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
button.ui-data-table__row:nth-child(odd) {
|
||||
.ui-data-table__data-row:nth-child(odd) {
|
||||
background: var(--ui-row-background);
|
||||
}
|
||||
button.ui-data-table__row:hover {
|
||||
.ui-data-table__data-row.is-selectable:hover {
|
||||
background: var(--ui-table-row-hover);
|
||||
outline: 1px solid var(--ui-table-row-hover-outline);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
button.ui-data-table__row.is-selected {
|
||||
background: var(--ui-table-row-selected);
|
||||
outline: 1px solid var(--ui-table-row-selected-outline);
|
||||
.ui-data-table__data-row.is-selected {
|
||||
background: var(--ui-table-selected-background);
|
||||
outline: 1px solid var(--ui-table-selected-outline);
|
||||
outline-offset: -1px;
|
||||
box-shadow: inset 0 0 0 1px var(--ui-table-row-selected-inset);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-table-selected-inset);
|
||||
}
|
||||
|
||||
.ui-data-table__data-row.is-disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.ui-data-table__row > * {
|
||||
@@ -132,6 +238,16 @@ button.ui-data-table__row.is-selected {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-data-table__row > .is-align-center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-data-table__row > .is-align-end {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ui-data-table__header {
|
||||
min-height: var(--ui-table-header-block-size);
|
||||
height: var(--ui-table-header-block-size);
|
||||
@@ -172,4 +288,57 @@ button.ui-data-table__row.is-selected {
|
||||
.ui-data-table__sort[aria-sort='descending'] i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ui-data-table--selection-gold {
|
||||
--ui-table-selected-background: var(--ui-table-row-selected);
|
||||
--ui-table-selected-outline: var(--ui-table-row-selected-outline);
|
||||
--ui-table-selected-inset: var(--ui-table-row-selected-inset);
|
||||
}
|
||||
|
||||
.ui-data-table--selection-blue {
|
||||
--ui-table-selected-background: var(--ui-table-row-hover);
|
||||
--ui-table-selected-outline: var(--ui-table-row-hover-outline);
|
||||
--ui-table-selected-inset: var(--ui-table-row-hover-outline);
|
||||
}
|
||||
|
||||
.ui-data-table--compact .ui-data-table__data-row {
|
||||
min-height: var(--ui-table-row-compact-block-size);
|
||||
font-size: var(--ui-table-compact-font-size);
|
||||
}
|
||||
|
||||
.ui-data-table__note,
|
||||
.ui-data-table__after-row,
|
||||
.ui-data-table__empty {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-height: var(--ui-table-note-row-min-block-size);
|
||||
align-items: center;
|
||||
padding: var(--ui-table-note-row-padding);
|
||||
border-bottom: 1px solid var(--ui-table-row-divider);
|
||||
background: var(--ui-table-note-row-background);
|
||||
color: var(--ui-table-note-row-foreground);
|
||||
}
|
||||
|
||||
.ui-data-table__empty {
|
||||
justify-content: center;
|
||||
min-height: var(--ui-table-empty-min-block-size);
|
||||
color: var(--ui-muted-foreground);
|
||||
}
|
||||
|
||||
.ui-data-table__summary {
|
||||
border-top: 1px solid var(--ui-table-summary-border);
|
||||
border-bottom: 0;
|
||||
background: var(--ui-table-summary-background);
|
||||
color: var(--ui-table-summary-foreground);
|
||||
}
|
||||
|
||||
.ui-data-table__footer {
|
||||
display: flex;
|
||||
min-height: var(--ui-table-footer-min-block-size);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: var(--ui-table-footer-padding);
|
||||
border-top: 1px solid var(--ui-table-footer-border);
|
||||
background: var(--ui-table-footer-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { DialogPortal, DialogRoot } from 'reka-ui'
|
||||
import { UiButton } from '../button'
|
||||
import UiDialogContent from './UiDialogContent.vue'
|
||||
import UiDialogDescription from './UiDialogDescription.vue'
|
||||
import UiDialogFooter from './UiDialogFooter.vue'
|
||||
import UiDialogHeader from './UiDialogHeader.vue'
|
||||
import UiDialogOverlay from './UiDialogOverlay.vue'
|
||||
import UiDialogTitle from './UiDialogTitle.vue'
|
||||
|
||||
export type UiActionDialogResult = 'ok' | 'reject' | 'cancel' | 'none'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible?: boolean
|
||||
header?: string | boolean
|
||||
mask?: boolean
|
||||
modal?: boolean
|
||||
drag?: 'header' | 'all' | 'none'
|
||||
okButton?: string | boolean
|
||||
rejectButton?: string | boolean
|
||||
cancelButton?: string | boolean
|
||||
closeOnEsc?: boolean
|
||||
defaultStatus?: UiActionDialogResult
|
||||
returnFocusElement?: HTMLElement | null
|
||||
scale?: number
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
header: true,
|
||||
mask: false,
|
||||
modal: false,
|
||||
drag: 'header',
|
||||
okButton: '确定',
|
||||
rejectButton: false,
|
||||
cancelButton: '取消',
|
||||
closeOnEsc: true,
|
||||
defaultStatus: 'cancel',
|
||||
scale: 1,
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
close: [status: UiActionDialogResult]
|
||||
ok: []
|
||||
reject: []
|
||||
cancel: []
|
||||
'update:visible': [visible: boolean]
|
||||
}>()
|
||||
|
||||
const result = ref<UiActionDialogResult>(props.defaultStatus)
|
||||
// Only a visible mask may make the dialog modal. A modal DialogRoot without an overlay
|
||||
// disables body pointer events and leaves the page looking inexplicably unresponsive.
|
||||
const hasBlockingOverlay = computed(() => props.mask)
|
||||
const contentStyle = computed<CSSProperties>(() => ({
|
||||
'--ui-dialog-scale': props.scale,
|
||||
'--ui-dialog-content-inline-size': 'max-content',
|
||||
'--ui-dialog-content-max-inline-size': 'var(--ui-dialog-viewport-inline-size)',
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) result.value = props.defaultStatus
|
||||
},
|
||||
)
|
||||
|
||||
function close(status: UiActionDialogResult = result.value) {
|
||||
result.value = status
|
||||
emits('update:visible', false)
|
||||
emits('close', status)
|
||||
}
|
||||
|
||||
function updateOpen(open: boolean) {
|
||||
if (open) emits('update:visible', true)
|
||||
else close()
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (!props.closeOnEsc) event.preventDefault()
|
||||
}
|
||||
|
||||
function handleInteractOutside(event: Event) {
|
||||
// With no mask and no outside-dismiss request, keep the window open while allowing
|
||||
// the underlying page to remain interactive. A visible mask always dismisses on click.
|
||||
if (!props.mask && !props.modal) event.preventDefault()
|
||||
}
|
||||
|
||||
function choose(status: Exclude<UiActionDialogResult, 'none'>) {
|
||||
if (status === 'ok') emits('ok')
|
||||
else if (status === 'reject') emits('reject')
|
||||
else emits('cancel')
|
||||
close(status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="visible" :modal="hasBlockingOverlay" @update:open="updateOpen">
|
||||
<DialogPortal>
|
||||
<UiDialogOverlay v-if="hasBlockingOverlay" />
|
||||
<UiDialogContent
|
||||
class="ui-action-dialog"
|
||||
:draggable="drag !== 'none'"
|
||||
:return-focus-element="returnFocusElement"
|
||||
:style="contentStyle"
|
||||
@escape-key-down="handleEscape"
|
||||
@interact-outside="handleInteractOutside"
|
||||
>
|
||||
<UiDialogHeader v-if="header" :closable="true">
|
||||
<UiDialogTitle>{{ header === true ? '提示' : header }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<UiDialogTitle v-else class="sr-only">提示</UiDialogTitle>
|
||||
<UiDialogDescription class="sr-only">{{
|
||||
header === true ? '提示' : header || '操作确认'
|
||||
}}</UiDialogDescription>
|
||||
|
||||
<div class="ui-action-dialog__body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<UiDialogFooter
|
||||
v-if="$slots.action || okButton || rejectButton || cancelButton"
|
||||
class="ui-action-dialog__footer"
|
||||
>
|
||||
<slot v-if="$slots.action" name="action" />
|
||||
<template v-else>
|
||||
<UiButton v-if="okButton" @click="choose('ok')">
|
||||
{{ okButton === true ? '确定' : okButton }}
|
||||
</UiButton>
|
||||
<UiButton v-if="rejectButton" variant="destructive" @click="choose('reject')">
|
||||
{{ rejectButton === true ? '拒绝' : rejectButton }}
|
||||
</UiButton>
|
||||
<UiButton v-if="cancelButton" variant="outline" @click="choose('cancel')">
|
||||
{{ cancelButton === true ? '取消' : cancelButton }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-action-dialog__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: min(100%, var(--ui-dialog-body-min-inline-size));
|
||||
min-height: var(--ui-dialog-body-min-block-size);
|
||||
max-width: var(--ui-dialog-viewport-inline-size);
|
||||
max-height: var(--ui-dialog-body-max-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ui-dialog-body-padding);
|
||||
overflow: auto;
|
||||
background: var(--ui-dialog-background);
|
||||
}
|
||||
|
||||
.ui-action-dialog__footer {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -13,18 +13,25 @@ const props = withDefaults(
|
||||
DialogContentProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
draggable?: boolean
|
||||
returnFocusElement?: HTMLElement | null
|
||||
}
|
||||
>(),
|
||||
{ draggable: true },
|
||||
)
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
const dragOffset = ref({ x: 0, y: 0 })
|
||||
const capturedReturnFocusElement = shallowRef<HTMLElement | null>(null)
|
||||
let activePointerId: number | undefined
|
||||
let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 }
|
||||
let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 }
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _class, draggable: _draggable, ...delegated } = props
|
||||
const {
|
||||
class: _class,
|
||||
draggable: _draggable,
|
||||
returnFocusElement: _returnFocusElement,
|
||||
...delegated
|
||||
} = props
|
||||
return delegated
|
||||
})
|
||||
|
||||
@@ -51,6 +58,36 @@ function resetPosition() {
|
||||
dragOffset.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
function handleOpenAutoFocus() {
|
||||
resetPosition()
|
||||
if (props.returnFocusElement) return
|
||||
|
||||
const activeElement = document.activeElement
|
||||
capturedReturnFocusElement.value =
|
||||
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
|
||||
}
|
||||
|
||||
function handleCloseAutoFocus(event: Event) {
|
||||
if (event.defaultPrevented) {
|
||||
capturedReturnFocusElement.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const explicitTarget = props.returnFocusElement
|
||||
const capturedTarget = capturedReturnFocusElement.value
|
||||
const target = explicitTarget?.isConnected
|
||||
? explicitTarget
|
||||
: capturedTarget?.isConnected
|
||||
? capturedTarget
|
||||
: null
|
||||
|
||||
capturedReturnFocusElement.value = null
|
||||
if (!target) return
|
||||
|
||||
event.preventDefault()
|
||||
nextTick(() => target.focus({ preventScroll: true }))
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (activePointerId !== event.pointerId) return
|
||||
const deltaX = event.clientX - dragOrigin.clientX
|
||||
@@ -106,7 +143,8 @@ onBeforeUnmount(() => {
|
||||
:style="dragStyle"
|
||||
:data-draggable="draggable ? '' : undefined"
|
||||
@pointerdown="handlePointerDown"
|
||||
@open-auto-focus="resetPosition"
|
||||
@open-auto-focus="handleOpenAutoFocus"
|
||||
@close-auto-focus="handleCloseAutoFocus"
|
||||
:class="
|
||||
cn(
|
||||
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
|
||||
@@ -120,8 +158,12 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.ui-dialog-content {
|
||||
width: min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size));
|
||||
max-height: calc(100dvh - 16px);
|
||||
width: var(
|
||||
--ui-dialog-content-inline-size,
|
||||
min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size))
|
||||
);
|
||||
max-width: var(--ui-dialog-content-max-inline-size, none);
|
||||
max-height: var(--ui-dialog-content-max-block-size, calc(100dvh - 16px));
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ui-dialog-border);
|
||||
border-radius: 0;
|
||||
@@ -131,9 +173,10 @@ onBeforeUnmount(() => {
|
||||
inset 0 0 0 1px var(--ui-dialog-inset);
|
||||
color: var(--ui-surface-foreground);
|
||||
transform: translate(
|
||||
calc(-50% + var(--ui-dialog-drag-x, 0px)),
|
||||
calc(-50% + var(--ui-dialog-drag-y, 0px))
|
||||
);
|
||||
calc(-50% + var(--ui-dialog-drag-x, 0px)),
|
||||
calc(-50% + var(--ui-dialog-drag-y, 0px))
|
||||
)
|
||||
scale(var(--ui-dialog-scale, 1));
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export { DialogClose, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
|
||||
export { default as UiActionDialog } from './UiActionDialog.vue'
|
||||
export type { UiActionDialogResult } from './UiActionDialog.vue'
|
||||
export { default as UiDialogContent } from './UiDialogContent.vue'
|
||||
export { default as UiDialogDescription } from './UiDialogDescription.vue'
|
||||
export { default as UiDialogFooter } from './UiDialogFooter.vue'
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties, HTMLAttributes } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from 'reka-ui'
|
||||
import { UiScrollArea } from '../scroll-area'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface UiFavoriteDrawerItem {
|
||||
value: string
|
||||
label: string
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
items: UiFavoriteDrawerItem[]
|
||||
title?: string
|
||||
expandedLabel?: string
|
||||
collapsedLabel?: string
|
||||
emptyText?: string
|
||||
disabled?: boolean
|
||||
maxVisibleItems?: number
|
||||
removeLabel?: (item: UiFavoriteDrawerItem) => string
|
||||
}>(),
|
||||
{
|
||||
title: '收藏',
|
||||
expandedLabel: '已展开',
|
||||
collapsedLabel: '已收起',
|
||||
emptyText: '暂无收藏',
|
||||
maxVisibleItems: 6,
|
||||
removeLabel: (item: UiFavoriteDrawerItem) => `移除 ${item.label}`,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [item: UiFavoriteDrawerItem]
|
||||
remove: [item: UiFavoriteDrawerItem]
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
|
||||
const scrollAreaStyle = computed(
|
||||
() =>
|
||||
({
|
||||
'--ui-favorite-drawer-visible-row-count': Math.max(
|
||||
1,
|
||||
Math.min(props.items.length, props.maxVisibleItems),
|
||||
),
|
||||
}) as CSSProperties,
|
||||
)
|
||||
|
||||
function selectItem(item: UiFavoriteDrawerItem) {
|
||||
if (!props.disabled && !item.disabled) emit('select', item)
|
||||
}
|
||||
|
||||
function removeItem(item: UiFavoriteDrawerItem) {
|
||||
if (!props.disabled && !item.disabled) emit('remove', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot
|
||||
v-model:open="open"
|
||||
:disabled="disabled"
|
||||
:class="cn('ui-favorite-drawer', disabled && 'is-disabled', props.class)"
|
||||
>
|
||||
<CollapsibleTrigger class="ui-favorite-drawer__trigger">
|
||||
<span class="ui-favorite-drawer__mark" aria-hidden="true"><slot name="icon">★</slot></span>
|
||||
<span class="ui-favorite-drawer__title">{{ title }}</span>
|
||||
<span class="ui-favorite-drawer__state">
|
||||
{{ open ? expandedLabel : collapsedLabel }}
|
||||
</span>
|
||||
<span class="ui-favorite-drawer__arrow" aria-hidden="true">{{ open ? '▲' : '▼' }}</span>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent class="ui-favorite-drawer__content">
|
||||
<UiScrollArea
|
||||
v-if="items.length"
|
||||
class="ui-favorite-drawer__scroll-area"
|
||||
:style="scrollAreaStyle"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
class="ui-favorite-drawer__row"
|
||||
:class="{ 'is-disabled': item.disabled }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-favorite-drawer__item"
|
||||
:disabled="disabled || item.disabled"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<slot name="item" :item="item">
|
||||
<img v-if="item.icon" :src="item.icon" alt="" aria-hidden="true" />
|
||||
<span>{{ item.label }}</span>
|
||||
</slot>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-favorite-drawer__remove"
|
||||
:aria-label="removeLabel(item)"
|
||||
:disabled="disabled || item.disabled"
|
||||
@click="removeItem(item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</UiScrollArea>
|
||||
<div v-else class="ui-favorite-drawer__empty">{{ emptyText }}</div>
|
||||
</CollapsibleContent>
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ui-favorite-drawer {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-border);
|
||||
background: var(--ui-disclosure-background);
|
||||
color: var(--ui-disclosure-row-foreground);
|
||||
|
||||
&[data-state='closed'] .ui-favorite-drawer__trigger {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__trigger {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: var(--ui-disclosure-header-min-block-size);
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
padding: 0 var(--ui-field-padding-inline);
|
||||
border: 0;
|
||||
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-divider);
|
||||
outline: 0;
|
||||
background: var(--ui-disclosure-header-background);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
background: var(
|
||||
--ui-favorite-drawer-header-hover-background,
|
||||
var(--ui-control-background-hover)
|
||||
);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(
|
||||
--ui-focus-outline-size,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
solid var(--ui-ring);
|
||||
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
|
||||
}
|
||||
|
||||
&:active:not(:disabled) > * {
|
||||
transform: translateY(
|
||||
var(
|
||||
--ui-pressed-offset,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__mark {
|
||||
color: var(--ui-favorite-drawer-mark, var(--ui-warning));
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__state {
|
||||
color: var(--ui-disclosure-state);
|
||||
font-size: var(--ui-disclosure-state-font-size);
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__arrow {
|
||||
width: var(--ui-navigation-indicator-inline-size);
|
||||
color: var(--ui-favorite-drawer-arrow, var(--ui-settings-section-foreground));
|
||||
font-size: var(--ui-badge-font-size);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__scroll-area {
|
||||
--ui-scroll-area-height: var(
|
||||
--ui-favorite-drawer-content-block-size,
|
||||
calc(var(--ui-disclosure-row-block-size) * var(--ui-favorite-drawer-visible-row-count))
|
||||
);
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__row {
|
||||
display: grid;
|
||||
height: var(--ui-disclosure-row-block-size);
|
||||
grid-template-columns: minmax(0, 1fr) var(--ui-control-height-compact);
|
||||
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-row-divider);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
background: var(--ui-disclosure-row-focus);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__item,
|
||||
.ui-favorite-drawer__remove {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: var(--ui-control-line-height);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(
|
||||
--ui-focus-outline-size,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
solid var(--ui-ring);
|
||||
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__item {
|
||||
gap: var(--ui-disclosure-row-gap);
|
||||
padding: 0 var(--ui-field-padding-inline);
|
||||
text-align: left;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--ui-disclosure-row-focus);
|
||||
}
|
||||
|
||||
img {
|
||||
width: var(--ui-disclosure-icon-size);
|
||||
height: var(--ui-disclosure-icon-size);
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__remove {
|
||||
justify-content: center;
|
||||
border-left: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-row-divider);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--ui-control-destructive-background);
|
||||
color: var(--ui-control-destructive-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-favorite-drawer__empty {
|
||||
display: flex;
|
||||
min-height: var(--ui-disclosure-row-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ui-disclosure-content-padding-block) var(--ui-field-padding-inline);
|
||||
color: var(--ui-disclosure-state);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiFavoriteDrawer } from './UiFavoriteDrawer.vue'
|
||||
export type { UiFavoriteDrawerItem } from './UiFavoriteDrawer.vue'
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive, type AsTag } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type LevelBadgeTone = 'default' | 'info' | 'success'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
as?: AsTag
|
||||
class?: HTMLAttributes['class']
|
||||
level?: number | string
|
||||
suffix?: string
|
||||
tone?: LevelBadgeTone
|
||||
}>(),
|
||||
{
|
||||
as: 'span',
|
||||
level: 1,
|
||||
suffix: '级',
|
||||
tone: 'default',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="cn('ui-level-badge', `ui-level-badge--${tone}`, props.class)">
|
||||
<slot>{{ level }}{{ suffix }}</slot>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-level-badge {
|
||||
display: inline-flex;
|
||||
min-width: var(--ui-level-badge-min-inline-size);
|
||||
min-height: var(--ui-level-badge-min-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: var(--ui-level-badge-padding-inline);
|
||||
border: 1px solid var(--ui-level-badge-border);
|
||||
background: var(--ui-level-badge-background);
|
||||
color: var(--ui-level-badge-foreground);
|
||||
font-size: var(--ui-level-badge-font-size);
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-level-badge--info {
|
||||
color: var(--ui-info);
|
||||
}
|
||||
|
||||
.ui-level-badge--success {
|
||||
color: var(--ui-success);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive, type AsTag } from 'reka-ui'
|
||||
import noticeBadgeAsset from '../../../../../design/src-web/rarity/img/sprite_interface.NPK/windowcommon.chn.img/301.PNG'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type NoticeBadgePlacement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
as?: AsTag
|
||||
class?: HTMLAttributes['class']
|
||||
label?: string
|
||||
placement?: NoticeBadgePlacement
|
||||
visible?: boolean
|
||||
}>(),
|
||||
{
|
||||
as: 'span',
|
||||
label: '有新通知',
|
||||
placement: 'top-end',
|
||||
visible: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="cn('ui-notice-badge', props.class)">
|
||||
<slot />
|
||||
<span
|
||||
v-if="visible"
|
||||
class="ui-notice-badge__marker"
|
||||
:class="`ui-notice-badge__marker--${placement}`"
|
||||
role="status"
|
||||
:aria-label="label"
|
||||
>
|
||||
<img :src="noticeBadgeAsset" alt="" aria-hidden="true" />
|
||||
</span>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-notice-badge {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: var(--ui-notice-badge-size);
|
||||
height: var(--ui-notice-badge-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker--top-start {
|
||||
top: var(--ui-notice-badge-offset);
|
||||
left: var(--ui-notice-badge-offset);
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker--top-end {
|
||||
top: var(--ui-notice-badge-offset);
|
||||
right: var(--ui-notice-badge-offset);
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker--bottom-start {
|
||||
bottom: var(--ui-notice-badge-offset);
|
||||
left: var(--ui-notice-badge-offset);
|
||||
}
|
||||
|
||||
.ui-notice-badge__marker--bottom-end {
|
||||
right: var(--ui-notice-badge-offset);
|
||||
bottom: var(--ui-notice-badge-offset);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiLevelBadge } from './UiLevelBadge.vue'
|
||||
export { default as UiNoticeBadge } from './UiNoticeBadge.vue'
|
||||
@@ -4,23 +4,40 @@ import { ProgressIndicator, ProgressRoot, type ProgressRootProps } from 'reka-ui
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ProgressRootProps & { class?: HTMLAttributes['class']; marker?: number }>(),
|
||||
defineProps<
|
||||
ProgressRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
/** Backward-compatible single milestone. Prefer `markers` for multiple thresholds. */
|
||||
marker?: number
|
||||
/** Milestones expressed as percentages from 0 to 100. */
|
||||
markers?: number[]
|
||||
}
|
||||
>(),
|
||||
{ max: 100 },
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _class, marker: _marker, ...delegated } = props
|
||||
const { class: _class, marker: _marker, markers: _markers, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
const percentage = computed(() =>
|
||||
Math.min(100, Math.max(0, ((props.modelValue ?? 0) / (props.max ?? 100)) * 100)),
|
||||
)
|
||||
const visibleMarkers = computed(() => {
|
||||
const values = props.markers ?? (props.marker == null ? [] : [props.marker])
|
||||
return [...new Set(values.map((value) => Math.min(100, Math.max(0, value))))]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)">
|
||||
<ProgressIndicator class="ui-progress__indicator" :style="{ width: `${percentage}%` }" />
|
||||
<span v-if="marker != null" class="ui-progress__marker" :style="{ left: `${marker}%` }" />
|
||||
<span
|
||||
v-for="milestone in visibleMarkers"
|
||||
:key="milestone"
|
||||
class="ui-progress__marker"
|
||||
:style="{ left: `${milestone}%` }"
|
||||
/>
|
||||
</ProgressRoot>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties, HTMLAttributes } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { TreeItem, TreeRoot } from 'reka-ui'
|
||||
import expandedIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/565.PNG'
|
||||
import collapsedIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/561.PNG'
|
||||
import clearIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/582.PNG'
|
||||
import searchIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/652.PNG'
|
||||
import { UiButton } from '../button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface UiSearchTreeNode {
|
||||
value: string
|
||||
label: string
|
||||
icon?: string
|
||||
expandedIcon?: string
|
||||
collapsedIcon?: string
|
||||
disabled?: boolean
|
||||
children?: UiSearchTreeNode[]
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
items: UiSearchTreeNode[]
|
||||
placeholder?: string
|
||||
searchLabel?: string
|
||||
refreshLabel?: string
|
||||
clearLabel?: string
|
||||
treeLabel?: string
|
||||
searchIcon?: string
|
||||
clearIcon?: string
|
||||
disabled?: boolean
|
||||
filter?: boolean
|
||||
defaultExpandAll?: boolean
|
||||
selectBranches?: boolean
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请输入关键词',
|
||||
searchLabel: '搜索',
|
||||
refreshLabel: '刷新',
|
||||
clearLabel: '清除搜索',
|
||||
treeLabel: '搜索结果',
|
||||
searchIcon: searchIconAsset,
|
||||
clearIcon: clearIconAsset,
|
||||
filter: false,
|
||||
defaultExpandAll: true,
|
||||
selectBranches: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [query: string]
|
||||
refresh: [query: string]
|
||||
select: [item: UiSearchTreeNode]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>()
|
||||
const query = defineModel<string>('query', { default: '' })
|
||||
const expanded = defineModel<string[]>('expanded', { default: () => [] })
|
||||
const committedQuery = ref('')
|
||||
const expansionInitialized = ref(false)
|
||||
|
||||
function findItem(
|
||||
items: UiSearchTreeNode[],
|
||||
value: string | undefined,
|
||||
): UiSearchTreeNode | undefined {
|
||||
for (const item of items) {
|
||||
if (item.value === value) return item
|
||||
const nested = item.children && findItem(item.children, value)
|
||||
if (nested) return nested
|
||||
}
|
||||
}
|
||||
|
||||
function collectBranchValues(items: UiSearchTreeNode[]): string[] {
|
||||
return items.flatMap((item) =>
|
||||
item.children?.length ? [item.value, ...collectBranchValues(item.children)] : [],
|
||||
)
|
||||
}
|
||||
|
||||
function filterItems(items: UiSearchTreeNode[], normalizedQuery: string): UiSearchTreeNode[] {
|
||||
return items.flatMap((item) => {
|
||||
const children = item.children ? filterItems(item.children, normalizedQuery) : []
|
||||
const matches = item.label.toLocaleLowerCase().includes(normalizedQuery)
|
||||
if (!matches && !children.length) return []
|
||||
return [{ ...item, children: matches ? item.children : children }]
|
||||
})
|
||||
}
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const normalizedQuery = committedQuery.value.trim().toLocaleLowerCase()
|
||||
if (!props.filter || !normalizedQuery) return props.items
|
||||
return filterItems(props.items, normalizedQuery)
|
||||
})
|
||||
|
||||
const selectedItem = computed(() => findItem(visibleItems.value, model.value))
|
||||
|
||||
function performSearch() {
|
||||
if (props.disabled) return
|
||||
committedQuery.value = query.value.trim()
|
||||
if (props.filter && committedQuery.value) {
|
||||
expanded.value = [...new Set([...expanded.value, ...collectBranchValues(visibleItems.value)])]
|
||||
}
|
||||
emit('search', query.value)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
if (props.disabled) return
|
||||
query.value = ''
|
||||
committedQuery.value = ''
|
||||
emit('search', '')
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!props.disabled) emit('refresh', query.value)
|
||||
}
|
||||
|
||||
function handleSelect(item: UiSearchTreeNode | undefined) {
|
||||
if (!item || props.disabled || item.disabled) return
|
||||
if (item.children?.length && !props.selectBranches) return
|
||||
|
||||
model.value = item.value
|
||||
emit('select', item)
|
||||
}
|
||||
|
||||
function preventParentSelection(event: Event, item: UiSearchTreeNode) {
|
||||
if (item.children?.length && !props.selectBranches) event.preventDefault()
|
||||
}
|
||||
|
||||
function nodeIcon(item: UiSearchTreeNode, isExpanded: boolean) {
|
||||
if (item.icon) return item.icon
|
||||
if (!item.children?.length) return undefined
|
||||
return isExpanded
|
||||
? (item.expandedIcon ?? expandedIconAsset)
|
||||
: (item.collapsedIcon ?? collapsedIconAsset)
|
||||
}
|
||||
|
||||
function depthStyle(level: number): CSSProperties {
|
||||
return { '--ui-search-tree-depth': Math.max(0, level - 1) } as CSSProperties
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
(items) => {
|
||||
if (expansionInitialized.value || !items.length) return
|
||||
if (props.defaultExpandAll && !expanded.value.length)
|
||||
expanded.value = collectBranchValues(items)
|
||||
expansionInitialized.value = true
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :class="cn('ui-search-tree', disabled && 'is-disabled', props.class)">
|
||||
<div class="ui-search-tree__toolbar">
|
||||
<img class="ui-search-tree__search-icon" :src="searchIcon" alt="" aria-hidden="true" />
|
||||
<div class="ui-search-tree__field">
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:aria-label="placeholder"
|
||||
@keydown.enter.prevent="performSearch"
|
||||
/>
|
||||
<button
|
||||
v-if="query"
|
||||
type="button"
|
||||
class="ui-search-tree__clear"
|
||||
:aria-label="clearLabel"
|
||||
:disabled="disabled"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<img :src="clearIcon" alt="" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<UiButton class="ui-search-tree__action" :disabled="disabled" @click="performSearch">
|
||||
{{ searchLabel }}
|
||||
</UiButton>
|
||||
<UiButton class="ui-search-tree__action" :disabled="disabled" @click="refresh">
|
||||
{{ refreshLabel }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<TreeRoot
|
||||
v-slot="{ flattenItems }"
|
||||
class="ui-search-tree__tree"
|
||||
:model-value="selectedItem"
|
||||
v-model:expanded="expanded"
|
||||
:items="visibleItems"
|
||||
:get-key="(item) => item.value"
|
||||
:get-children="(item) => item.children"
|
||||
:aria-label="treeLabel"
|
||||
:aria-disabled="disabled"
|
||||
@update:model-value="handleSelect"
|
||||
>
|
||||
<TreeItem
|
||||
v-for="item in flattenItems"
|
||||
:key="item._id"
|
||||
v-slot="{ isExpanded }"
|
||||
v-bind="item.bind"
|
||||
class="ui-search-tree__row"
|
||||
:class="{
|
||||
'is-parent': item.hasChildren,
|
||||
'is-root': item.level === 1,
|
||||
}"
|
||||
:disabled="disabled || item.value.disabled"
|
||||
:style="depthStyle(item.level)"
|
||||
@select="preventParentSelection($event, item.value)"
|
||||
>
|
||||
<slot name="node" :item="item.value" :expanded="isExpanded" :level="item.level">
|
||||
<img
|
||||
v-if="nodeIcon(item.value, isExpanded)"
|
||||
:src="nodeIcon(item.value, isExpanded)"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ item.value.label }}</span>
|
||||
</slot>
|
||||
</TreeItem>
|
||||
</TreeRoot>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ui-search-tree {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-search-tree__toolbar {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
}
|
||||
|
||||
.ui-search-tree__search-icon {
|
||||
width: var(--ui-search-tree-icon-size, var(--ui-badge-height));
|
||||
height: var(--ui-search-tree-icon-size, var(--ui-badge-height));
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.ui-search-tree__field {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: var(--ui-control-height-default);
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding-inline-start: var(--ui-field-padding-inline);
|
||||
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-input);
|
||||
background: var(--ui-field-background);
|
||||
box-shadow: inset 0 0 0
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-inset);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--ui-ring);
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-focus-inset),
|
||||
0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
var(--ui-field-focus-edge);
|
||||
}
|
||||
|
||||
input {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--ui-foreground);
|
||||
font: inherit;
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
line-height: var(--ui-control-line-height);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ui-muted-foreground);
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-search-tree__clear {
|
||||
display: inline-flex;
|
||||
width: var(--ui-control-height-compact);
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(
|
||||
--ui-focus-outline-size,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
solid var(--ui-ring);
|
||||
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
|
||||
}
|
||||
|
||||
img {
|
||||
width: var(--ui-search-tree-clear-size, var(--ui-dialog-close-glyph-block-size));
|
||||
height: var(--ui-search-tree-clear-size, var(--ui-dialog-close-glyph-block-size));
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-search-tree__action {
|
||||
min-width: var(
|
||||
--ui-search-tree-action-min-inline-size,
|
||||
calc(var(--ui-control-height-default) * 2)
|
||||
);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.ui-search-tree__tree {
|
||||
box-sizing: border-box;
|
||||
height: var(--ui-search-tree-block-size, var(--ui-autocomplete-max-block-size));
|
||||
margin-block-start: var(--ui-autocomplete-trigger-gap);
|
||||
margin-inline-start: var(--ui-search-tree-indent, var(--ui-navigation-icon-size));
|
||||
padding-block: var(--ui-autocomplete-trigger-gap);
|
||||
overflow-y: auto;
|
||||
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-border);
|
||||
outline: 0;
|
||||
background: var(--ui-tree-background);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ui-search-tree__row {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: var(--ui-search-tree-row-block-size, var(--ui-option-height));
|
||||
align-items: center;
|
||||
gap: var(--ui-disclosure-row-gap);
|
||||
padding: 0 var(--ui-field-padding-inline);
|
||||
padding-inline-start: calc(
|
||||
var(--ui-field-padding-inline) + var(--ui-search-tree-depth) *
|
||||
var(--ui-search-tree-depth-step, var(--ui-tree-depth-step))
|
||||
);
|
||||
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
|
||||
var(--ui-disclosure-row-divider);
|
||||
outline: 0;
|
||||
color: var(--ui-disclosure-row-foreground);
|
||||
line-height: var(--ui-control-line-height);
|
||||
cursor: pointer;
|
||||
|
||||
&.is-parent:not(.is-root) {
|
||||
color: var(--ui-tree-branch-foreground);
|
||||
}
|
||||
|
||||
&:hover:not([data-disabled]),
|
||||
&:focus-visible:not([data-disabled]) {
|
||||
background: var(--ui-disclosure-row-focus);
|
||||
color: var(--ui-option-highlight-foreground);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(
|
||||
--ui-focus-outline-size,
|
||||
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
|
||||
)
|
||||
solid var(--ui-ring);
|
||||
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
background: var(--ui-disclosure-row-selected);
|
||||
box-shadow: inset var(--ui-autocomplete-trigger-gap) 0 0 var(--ui-selected);
|
||||
color: var(--ui-disclosure-row-selected-foreground);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
img {
|
||||
width: var(--ui-search-tree-node-icon-size, var(--ui-disclosure-icon-size));
|
||||
height: var(--ui-search-tree-node-icon-size, var(--ui-disclosure-icon-size));
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-search-tree.is-disabled {
|
||||
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiSearchTree } from './UiSearchTree.vue'
|
||||
export type { UiSearchTreeNode } from './UiSearchTree.vue'
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type SettingsDropdownAlign = 'start' | 'center' | 'end'
|
||||
type SettingsDropdownSide = 'top' | 'right' | 'bottom' | 'left'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
align?: SettingsDropdownAlign
|
||||
class?: HTMLAttributes['class']
|
||||
collisionPadding?: number
|
||||
disabled?: boolean
|
||||
modal?: boolean
|
||||
side?: SettingsDropdownSide
|
||||
sideOffset?: number
|
||||
title?: string
|
||||
}>(),
|
||||
{
|
||||
align: 'center',
|
||||
collisionPadding: 8,
|
||||
disabled: false,
|
||||
modal: true,
|
||||
side: 'bottom',
|
||||
sideOffset: 4,
|
||||
title: '设置菜单',
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-model:open="open" :modal="modal">
|
||||
<DropdownMenuTrigger v-if="$slots.trigger" as-child :disabled="disabled">
|
||||
<slot name="trigger" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
:align="align"
|
||||
:side="side"
|
||||
:side-offset="sideOffset"
|
||||
:collision-padding="collisionPadding"
|
||||
:class="cn('ui-settings-dropdown', props.class)"
|
||||
@close-auto-focus="emits('closeAutoFocus', $event)"
|
||||
@escape-key-down="emits('escapeKeyDown', $event)"
|
||||
@focus-outside="emits('focusOutside', $event)"
|
||||
@interact-outside="emits('interactOutside', $event)"
|
||||
@pointer-down-outside="emits('pointerDownOutside', $event)"
|
||||
>
|
||||
<DropdownMenuLabel class="ui-settings-dropdown__title">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</DropdownMenuLabel>
|
||||
<div class="ui-settings-dropdown__items">
|
||||
<slot />
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global([data-reka-popper-content-wrapper]:has(> .ui-settings-dropdown)) {
|
||||
z-index: var(--ui-settings-dropdown-z-index) !important;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown {
|
||||
position: relative;
|
||||
z-index: var(--ui-settings-dropdown-z-index);
|
||||
width: min(
|
||||
var(--ui-settings-dropdown-viewport-inline-size),
|
||||
var(--ui-settings-dropdown-inline-size)
|
||||
);
|
||||
padding: var(--ui-settings-dropdown-padding);
|
||||
border: 1px solid var(--ui-settings-dropdown-border);
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
background: var(--ui-settings-dropdown-background);
|
||||
box-shadow: var(--ui-settings-dropdown-shadow);
|
||||
color: var(--ui-settings-dropdown-item-foreground);
|
||||
transform-origin: var(--reka-dropdown-menu-content-transform-origin);
|
||||
}
|
||||
|
||||
.ui-settings-dropdown[data-state='open'] {
|
||||
animation: ui-settings-dropdown-in 100ms ease-out;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown__title {
|
||||
display: flex;
|
||||
min-height: var(--ui-settings-dropdown-title-min-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--ui-settings-dropdown-title-gap);
|
||||
padding-inline: var(--ui-settings-dropdown-title-padding-inline);
|
||||
border: 1px solid var(--ui-settings-dropdown-title-border);
|
||||
background: var(--ui-settings-dropdown-title-background);
|
||||
color: var(--ui-settings-dropdown-title-foreground);
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
font-weight: 400;
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown__items {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@keyframes ui-settings-dropdown-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(var(--ui-settings-dropdown-enter-offset));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ui-settings-dropdown[data-state='open'] {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
type DropdownMenuItemEmits,
|
||||
type DropdownMenuItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
danger?: boolean
|
||||
iconAlt?: string
|
||||
iconSrc?: string
|
||||
}
|
||||
>()
|
||||
const emits = defineEmits<DropdownMenuItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const {
|
||||
class: _class,
|
||||
danger: _danger,
|
||||
iconAlt: _iconAlt,
|
||||
iconSrc: _iconSrc,
|
||||
...delegated
|
||||
} = props
|
||||
return delegated
|
||||
})
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'ui-settings-dropdown-item',
|
||||
props.danger && 'ui-settings-dropdown-item--danger',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="ui-settings-dropdown-item__icon" aria-hidden="true">
|
||||
<slot name="icon">
|
||||
<img v-if="iconSrc" :src="iconSrc" :alt="iconAlt" />
|
||||
</slot>
|
||||
</span>
|
||||
<span class="ui-settings-dropdown-item__label"><slot /></span>
|
||||
<span v-if="$slots.trailing" class="ui-settings-dropdown-item__trailing">
|
||||
<slot name="trailing" />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-settings-dropdown-item {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--ui-settings-dropdown-icon-column-size) minmax(0, 1fr)
|
||||
auto;
|
||||
min-height: var(--ui-settings-dropdown-item-min-block-size);
|
||||
align-items: center;
|
||||
padding: 0 var(--ui-settings-dropdown-item-padding-inline) 0 0;
|
||||
border: 1px solid var(--ui-settings-dropdown-item-border);
|
||||
outline: none;
|
||||
background: var(--ui-settings-dropdown-item-background);
|
||||
color: var(--ui-settings-dropdown-item-foreground);
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
line-height: var(--ui-control-line-height);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item + .ui-settings-dropdown-item {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item[data-highlighted] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
outline: var(--ui-settings-dropdown-focus-outline-size) solid
|
||||
var(--ui-settings-dropdown-focus-border);
|
||||
outline-offset: var(--ui-settings-dropdown-focus-outline-offset);
|
||||
background: var(--ui-settings-dropdown-focus-background);
|
||||
color: var(--ui-settings-dropdown-focus-foreground);
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item[data-disabled] {
|
||||
opacity: var(--ui-settings-dropdown-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item--danger {
|
||||
color: var(--ui-menu-danger);
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item__icon {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item__icon :deep(img) {
|
||||
display: block;
|
||||
max-width: var(--ui-settings-dropdown-icon-max-size);
|
||||
max-height: var(--ui-settings-dropdown-icon-max-size);
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-settings-dropdown-item__trailing {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-left: var(--ui-control-gap);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UiSettingsDropdown } from './UiSettingsDropdown.vue'
|
||||
export { default as UiSettingsDropdownItem } from './UiSettingsDropdownItem.vue'
|
||||
@@ -25,10 +25,15 @@ const props = withDefaults(
|
||||
placeholder?: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
selectParents?: boolean
|
||||
}>(),
|
||||
{ placeholder: '请选择', label: '树形选择' },
|
||||
{ placeholder: '请选择', label: '树形选择', selectParents: false },
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
select: [item: UiTreeSelectItem]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>()
|
||||
const open = ref(false)
|
||||
const expanded = ref<string[]>([])
|
||||
@@ -80,13 +85,14 @@ watch(
|
||||
)
|
||||
|
||||
function handleSelect(item: UiTreeSelectItem | undefined) {
|
||||
if (!item || item.disabled || item.children?.length) return
|
||||
if (!item || item.disabled || (item.children?.length && !props.selectParents)) return
|
||||
model.value = item.value
|
||||
open.value = false
|
||||
emits('select', item)
|
||||
if (!item.children?.length) open.value = false
|
||||
}
|
||||
|
||||
function preventParentSelection(event: Event, item: UiTreeSelectItem) {
|
||||
if (item.children?.length) event.preventDefault()
|
||||
if (item.children?.length && !props.selectParents) event.preventDefault()
|
||||
}
|
||||
|
||||
function depthStyle(level: number): CSSProperties {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
|
||||
import {
|
||||
UiDialogContent,
|
||||
UiDialogDescription,
|
||||
UiDialogFooter,
|
||||
UiDialogHeader,
|
||||
UiDialogOverlay,
|
||||
UiDialogTitle,
|
||||
} from '../dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type UtilitySheetSize = 'compact' | 'default' | 'wide'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
closeLabel?: string
|
||||
closable?: boolean
|
||||
draggable?: boolean
|
||||
focused?: boolean
|
||||
modal?: boolean
|
||||
overlay?: boolean
|
||||
size?: UtilitySheetSize
|
||||
title?: string
|
||||
}>(),
|
||||
{
|
||||
closeLabel: '关闭工具窗口',
|
||||
closable: true,
|
||||
draggable: true,
|
||||
focused: true,
|
||||
modal: true,
|
||||
overlay: true,
|
||||
size: 'default',
|
||||
title: '工具窗口',
|
||||
},
|
||||
)
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const returnFocusElement = shallowRef<HTMLElement | null>(null)
|
||||
const hasBlockingOverlay = computed(() => props.modal && props.overlay)
|
||||
|
||||
watch(
|
||||
open,
|
||||
(value) => {
|
||||
if (!value) return
|
||||
const activeElement = document.activeElement
|
||||
returnFocusElement.value =
|
||||
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-model:open="open" :modal="hasBlockingOverlay">
|
||||
<DialogTrigger v-if="$slots.trigger" as-child>
|
||||
<slot name="trigger" />
|
||||
</DialogTrigger>
|
||||
<DialogPortal>
|
||||
<UiDialogOverlay v-if="hasBlockingOverlay" />
|
||||
<UiDialogContent
|
||||
:draggable="draggable"
|
||||
:return-focus-element="returnFocusElement"
|
||||
:class="cn('ui-utility-sheet', `ui-utility-sheet--${size}`, props.class)"
|
||||
>
|
||||
<UiDialogHeader :closable="closable" :close-label="closeLabel" :focused="focused">
|
||||
<UiDialogTitle
|
||||
><slot name="title">{{ title }}</slot></UiDialogTitle
|
||||
>
|
||||
<template v-if="$slots.actions" #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</UiDialogHeader>
|
||||
<UiDialogDescription class="sr-only"> {{ title }}的配置与快捷操作 </UiDialogDescription>
|
||||
|
||||
<div class="ui-utility-sheet__body">
|
||||
<slot name="body"><slot /></slot>
|
||||
</div>
|
||||
|
||||
<UiDialogFooter v-if="$slots.footer" class="ui-utility-sheet__footer">
|
||||
<slot name="footer" />
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-utility-sheet {
|
||||
width: min(
|
||||
var(--ui-utility-sheet-viewport-inline-size),
|
||||
var(--ui-utility-sheet-active-inline-size)
|
||||
);
|
||||
max-width: none;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.ui-utility-sheet--compact {
|
||||
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-compact-inline-size);
|
||||
--ui-utility-sheet-active-body-min-block-size: var(
|
||||
--ui-utility-sheet-compact-body-min-block-size
|
||||
);
|
||||
}
|
||||
|
||||
.ui-utility-sheet--default {
|
||||
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-default-inline-size);
|
||||
--ui-utility-sheet-active-body-min-block-size: var(
|
||||
--ui-utility-sheet-default-body-min-block-size
|
||||
);
|
||||
}
|
||||
|
||||
.ui-utility-sheet--wide {
|
||||
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-wide-inline-size);
|
||||
--ui-utility-sheet-active-body-min-block-size: var(--ui-utility-sheet-wide-body-min-block-size);
|
||||
}
|
||||
|
||||
.ui-utility-sheet__body {
|
||||
min-width: 0;
|
||||
min-height: var(--ui-utility-sheet-active-body-min-block-size);
|
||||
max-height: var(--ui-utility-sheet-body-max-block-size);
|
||||
padding: var(--ui-utility-sheet-body-padding);
|
||||
overflow: auto;
|
||||
background: var(--ui-utility-sheet-body-background);
|
||||
}
|
||||
|
||||
.ui-utility-sheet__footer {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as UiUtilitySheet } from './UiUtilitySheet.vue'
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogPortal, DialogRoot } from 'reka-ui'
|
||||
import {
|
||||
UiDialogContent,
|
||||
UiDialogDescription,
|
||||
UiDialogHeader,
|
||||
UiDialogOverlay,
|
||||
UiDialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { provideShow } from '@/components/hooks/show'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
}>(),
|
||||
{
|
||||
title: '导入',
|
||||
},
|
||||
)
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
let returnFocusElement: HTMLElement | null = null
|
||||
|
||||
// The existing import forms reset themselves through this injection whenever their dialog opens.
|
||||
provideShow(open)
|
||||
|
||||
watch(open, (value) => {
|
||||
if (value && document.activeElement instanceof HTMLElement) {
|
||||
returnFocusElement = document.activeElement
|
||||
}
|
||||
})
|
||||
|
||||
function keepOpenOnOutsideInteraction(event: Event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function restoreTriggerFocus(event: Event) {
|
||||
event.preventDefault()
|
||||
nextTick(() => returnFocusElement?.focus())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-model:open="open" modal>
|
||||
<DialogPortal>
|
||||
<UiDialogOverlay />
|
||||
<UiDialogContent
|
||||
class="character-import-dialog"
|
||||
@interact-outside="keepOpenOnOutsideInteraction"
|
||||
@pointer-down-outside="keepOpenOnOutsideInteraction"
|
||||
@close-auto-focus="restoreTriggerFocus"
|
||||
>
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>{{ title }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<UiDialogDescription class="character-import-dialog__description">
|
||||
{{ title }}窗口
|
||||
</UiDialogDescription>
|
||||
<div class="character-import-dialog__body">
|
||||
<slot />
|
||||
</div>
|
||||
</UiDialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.character-import-dialog {
|
||||
--ui-dialog-viewport-inline-size: calc(100vw - 32px);
|
||||
--ui-dialog-max-inline-size: 360px;
|
||||
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.character-import-dialog__description {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.character-import-dialog__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 128px;
|
||||
max-height: calc(100dvh - var(--ui-dialog-header-block-size) - 32px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,8 @@ import api from '@/api'
|
||||
import { useDialog } from '@/components/hooks/dialog'
|
||||
import { isShow, onShow } from '@/components/hooks/show'
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
import { UiInput } from '@/components/ui/input'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -48,20 +50,28 @@ export default defineComponent({
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="h-30 w-80 py-2 px-4 text-#e9c556">
|
||||
<div class="flex justify-around items-center">
|
||||
助手社区ID
|
||||
<calc-input class="!w-60" v-model={uid.value} />
|
||||
<div class="box-border flex min-h-30 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
|
||||
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
|
||||
<span>助手社区ID</span>
|
||||
<UiInput class="min-w-0" v-model={uid.value} />
|
||||
</label>
|
||||
<div class="leading-5">
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
点击导入后会从助手通过文字识别获取打造
|
||||
</p>
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
部分打造无法获取,此处仅做快捷导入
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
|
||||
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
|
||||
</div>
|
||||
<div class="flex justify-around items-center">
|
||||
<calc-button onClick={execute} disabled={isLoading.value}>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<UiButton onClick={() => execute()} disabled={isLoading.value}>
|
||||
导入
|
||||
</calc-button>
|
||||
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
|
||||
</UiButton>
|
||||
<UiButton variant="outline" onClick={() => (visible.value = false)}>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDialog } from '@/components/hooks/dialog'
|
||||
import { isShow, onShow } from '@/components/hooks/show'
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { type IHelperRoleInfo } from '@/api/info/type'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -54,22 +55,30 @@ export default defineComponent({
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="h-45 w-80 py-2 px-4 text-#e9c556">
|
||||
<div class="flex justify-around items-center flex-col gap-1">
|
||||
<div class="box-border flex min-h-45 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
|
||||
<div class="flex flex-col items-center justify-center gap-1 text-center">
|
||||
<div>检测到以下信息,是否导入助手数据?</div>
|
||||
<div class="text-#DED29A">{props.alter?.roleName}</div>
|
||||
<div class="text-#908150">{props.alter?.roleJob}</div>
|
||||
<div class="text-#3ea74e">名望:{props.alter?.fame}</div>
|
||||
<div class="text-[color:var(--ui-primary-foreground)]">{props.alter?.roleName}</div>
|
||||
<div class="text-[color:var(--ui-muted-foreground)]">{props.alter?.roleJob}</div>
|
||||
<div class="text-[color:var(--ui-success)]">名望:{props.alter?.fame}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
|
||||
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
|
||||
<div class="leading-5">
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
点击导入后会从助手通过文字识别获取打造
|
||||
</p>
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
部分打造无法获取,此处仅做快捷导入
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-around items-center">
|
||||
<calc-button onClick={execute} disabled={isLoading.value}>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<UiButton onClick={() => execute()} disabled={isLoading.value}>
|
||||
导入
|
||||
</calc-button>
|
||||
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
|
||||
</UiButton>
|
||||
<UiButton variant="outline" onClick={() => (visible.value = false)}>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,8 @@ import api from '@/api'
|
||||
import { useDialog } from '@/components/hooks/dialog'
|
||||
import { isShow, onShow } from '@/components/hooks/show'
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
import { UiInput } from '@/components/ui/input'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -44,23 +46,28 @@ export default defineComponent({
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="h-35 w-80 py-2 px-4 text-#e9c556">
|
||||
<div class="flex justify-around items-center">
|
||||
技能加点代码
|
||||
<calc-input class="!w-60" v-model={skillCode.value} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-#e9c556/80">
|
||||
<div class="box-border flex min-h-35 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
|
||||
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
|
||||
<span>技能加点代码</span>
|
||||
<UiInput class="min-w-0" v-model={skillCode.value} />
|
||||
</label>
|
||||
<div class="leading-5">
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
点击导入后会根据规则解析出技能加点、技能进化、技能强化
|
||||
</p>
|
||||
<p class="text-#e9c556/80">* 部分技能可能存在无法识别的情况,需要手动调整</p>
|
||||
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
|
||||
*
|
||||
部分技能可能存在无法识别的情况,需要手动调整
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-around items-center">
|
||||
<calc-button onClick={execute} disabled={!skillCode.value || isLoading.value}>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<UiButton onClick={() => execute()} disabled={!skillCode.value || isLoading.value}>
|
||||
导入
|
||||
</calc-button>
|
||||
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
|
||||
</UiButton>
|
||||
<UiButton variant="outline" onClick={() => (visible.value = false)}>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div class="mx-10px">
|
||||
<calc-collapse title="武器/称号/宠物/秘宝">
|
||||
<calc-tabs v-model="anotherTab" class="!mb-2">
|
||||
<calc-tab :value="0">武器</calc-tab>
|
||||
<calc-tab :value="1">称号/宠物/秘宝</calc-tab>
|
||||
<calc-tab :value="2" v-if="infoStore.infos?.alter.includes('vegabond')">副武器</calc-tab>
|
||||
</calc-tabs>
|
||||
<Weapon v-show="anotherTab === 0"></Weapon>
|
||||
<Else v-show="anotherTab === 1"></Else>
|
||||
<SubWeapon v-show="anotherTab === 2"></SubWeapon>
|
||||
</calc-collapse>
|
||||
<calc-collapse title="套装/通宝">
|
||||
<calc-tabs v-model="equTab" class="!mb-2">
|
||||
<calc-tab :value="0">套装</calc-tab>
|
||||
<calc-tab :value="1">通宝</calc-tab>
|
||||
</calc-tabs>
|
||||
<Suit v-show="equTab === 0"></Suit>
|
||||
<Universal v-show="equTab === 1"></Universal>
|
||||
</calc-collapse>
|
||||
<calc-collapse title="誓约">
|
||||
<calc-tabs v-model="stoneTab" class="!mb-2">
|
||||
<calc-tab :value="0">套装</calc-tab>
|
||||
<calc-tab :value="1">通宝</calc-tab>
|
||||
<calc-tab :value="2">其他</calc-tab>
|
||||
</calc-tabs>
|
||||
<Stone v-show="stoneTab == 0"></Stone>
|
||||
<StoneUniversal v-show="stoneTab == 1"></StoneUniversal>
|
||||
<StoneOld v-show="stoneTab == 2"></StoneOld>
|
||||
</calc-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="Index">
|
||||
import Suit from './components/Suit.vue'
|
||||
import Weapon from './components/Weapon.vue'
|
||||
import Stone from './components/Stone.vue'
|
||||
import Universal from './components/Universal.vue'
|
||||
import StoneUniversal from './components/StoneUniversal.vue'
|
||||
import StoneOld from './components/StoneOld.vue'
|
||||
import Else from './components/Else.vue'
|
||||
import { useInfoStore } from '@/stores'
|
||||
import SubWeapon from './components/SubWeapon.vue'
|
||||
|
||||
const infoStore = useInfoStore()
|
||||
const equTab = ref(0)
|
||||
const stoneTab = ref(0)
|
||||
const anotherTab = ref(0)
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(equ, _) in equs" :key="_">
|
||||
<EquipmentIcon
|
||||
:equipment="equ"
|
||||
:inactive="configStore.config.equips[equ?.itemDetailType ?? '']?.id != equ?.id"
|
||||
@click="configStore.chooseEqu(equ)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const equs = computed(() => {
|
||||
const items = infoStore.equips.filter((equip) => ['称号', '宠物'].includes(equip.itemDetailType))
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
|
||||
const equList = items.filter((item) => item.itemDetailType === type)
|
||||
equList.sort((a, b) => {
|
||||
if (a.itemType !== b.itemType) {
|
||||
return b.itemType.localeCompare(a.itemType)
|
||||
}
|
||||
const rarityOrder = ['传说', '神器', '稀有']
|
||||
return rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity)
|
||||
})
|
||||
res.push(...equList)
|
||||
const remind = rowCount - (res.length % rowCount)
|
||||
res.push(...Array(remind + rowCount).fill(undefined))
|
||||
})
|
||||
const treasure = infoStore.equips.filter((equip) => equip.categorize.includes('秘宝'))
|
||||
res.push(...treasure, ...Array(rowCount - treasure.length).fill(undefined))
|
||||
return res
|
||||
})
|
||||
</script>
|
||||
@@ -1,97 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-10px pb-10px">
|
||||
<!-- 套装选择tab -->
|
||||
<div class="flex flex-wrap w-100%">
|
||||
<template v-for="(suit, _) in suits" :key="_">
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
class="w-35px h-35px object-contain"
|
||||
:src="getImageURL('equipment' + suit.imageUrl)"
|
||||
:alt="suit.name"
|
||||
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
|
||||
@click="curSuit = suit.value"
|
||||
@dblclick="handleDoubleClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<EquipmentRaritySelect v-model="curRarity" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(equip, _) in suitEquipments" :key="_">
|
||||
<template v-if="equip?.rarity == curRarity || !curRarity">
|
||||
<EquipmentIcon
|
||||
:equipment="equip"
|
||||
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
|
||||
@click="configStore.chooseEqu(equip, true)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<EquipmentIcon />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
const curSuit = ref(16201)
|
||||
|
||||
const curRarity = ref()
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
|
||||
const suits = computed(() => {
|
||||
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
|
||||
infoStore.suits.forEach((a) => {
|
||||
if (!uniqueSuits.has(a.suitId)) {
|
||||
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
|
||||
}
|
||||
})
|
||||
return Array.from(uniqueSuits.values())
|
||||
})
|
||||
|
||||
const suitEquipments = computed(() => {
|
||||
const equs = infoStore.stones.filter(
|
||||
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
|
||||
)
|
||||
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
const raritiyList = ['神器', '传说', '史诗']
|
||||
|
||||
raritiyList.forEach((rarity) => {
|
||||
const equ: (IEquipment | undefined)[] = equs.filter((a) => a.rarity === rarity)
|
||||
equ.splice(5, 0, undefined)
|
||||
equ.splice(9, 0, undefined)
|
||||
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
|
||||
})
|
||||
return res
|
||||
})
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (!curRarity.value) return
|
||||
if (curRarity.value == '太初') return
|
||||
suitEquipments.value
|
||||
.filter((a) => !!a && a.rarity == curRarity.value)
|
||||
.splice(0, 11)
|
||||
.forEach((a) => {
|
||||
configStore.chooseEqu(a, true)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(equip, _) in equs" :key="_">
|
||||
<EquipmentIcon
|
||||
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
|
||||
:equipment="equip"
|
||||
@click="configStore.chooseEqu(equip, true)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup StoneUniversal>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const equs = computed(() => {
|
||||
const items = infoStore.stones.filter((equip) => equip.categorize.includes('通宝'))
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
const raritiyList = ['神器', '史诗']
|
||||
const categorize = Array.from(new Set(items.map((item) => item.categorize)))
|
||||
raritiyList.forEach((rarity) => {
|
||||
categorize.forEach((cat) => {
|
||||
const equ: (IEquipment | undefined)[] = items.filter(
|
||||
(a) => a.rarity === rarity && a.categorize === cat,
|
||||
)
|
||||
equ.splice(5, 0, undefined)
|
||||
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
|
||||
})
|
||||
})
|
||||
return res
|
||||
})
|
||||
</script>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(weapon, _) in weapons" :key="_">
|
||||
<EquipmentIcon
|
||||
:equipment="weapon"
|
||||
:inactive="configStore.config.equips['副武器']?.id != weapon?.id"
|
||||
@click="configStore.chooseEqu(weapon, false, true)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const weapons = computed(() => {
|
||||
const items = infoStore.equips.filter(
|
||||
(equip) => equip.itemType === '武器' && equip.itemDetailType == '光剑',
|
||||
)
|
||||
// console.log(items)
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
|
||||
const weapon = items.filter((item) => item.itemDetailType === type)
|
||||
res.push(
|
||||
...weapon.filter((item) => item.categorize === '制式武器'),
|
||||
undefined,
|
||||
...weapon.filter((item) => item.categorize === '传世武器'),
|
||||
)
|
||||
const remind = rowCount - (res.length % rowCount)
|
||||
res.push(...Array(remind).fill(undefined))
|
||||
})
|
||||
return res
|
||||
})
|
||||
</script>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-10px pb-10px">
|
||||
<!-- 套装选择tab -->
|
||||
<div class="flex flex-wrap w-100%">
|
||||
<template v-for="(suit, _) in suits" :key="_">
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
class="w-35px h-35px object-contain"
|
||||
:src="getImageURL('equipment' + suit.imageUrl)"
|
||||
:alt="suit.name"
|
||||
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
|
||||
@click="curSuit = suit.value"
|
||||
@dblclick="handleDoubleClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<EquipmentRaritySelect v-model="curRarity" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(equip, _) in suitEquipments" :key="_">
|
||||
<template v-if="equip?.rarity == curRarity || !curRarity">
|
||||
<EquipmentIcon
|
||||
:equipment="equip"
|
||||
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
|
||||
@click="configStore.chooseEqu(equip)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<EquipmentIcon />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
const curSuit = ref(16201)
|
||||
|
||||
const curRarity = ref()
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
|
||||
const suits = computed(() => {
|
||||
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
|
||||
infoStore.suits.forEach((a) => {
|
||||
if (!uniqueSuits.has(a.suitId)) {
|
||||
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
|
||||
}
|
||||
})
|
||||
return Array.from(uniqueSuits.values())
|
||||
})
|
||||
|
||||
const suitEquipments = computed(() => {
|
||||
const equs = infoStore.equips.filter(
|
||||
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
|
||||
)
|
||||
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
const raritiyList = ['神器', '传说', '史诗']
|
||||
raritiyList.forEach((rarity) => {
|
||||
const equ: (IEquipment | undefined)[] = equs.filter(
|
||||
(a) => a.rarity === rarity && !a.name.includes('黑牙'),
|
||||
)
|
||||
equ.splice(5, 0, undefined)
|
||||
equ.splice(9, 0, undefined)
|
||||
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
|
||||
})
|
||||
const remind = equs
|
||||
.filter((a) => a.rarity == '太初' || a.name.includes('黑牙'))
|
||||
.sort((a, b) => (a.rarity == '太初' ? 1 : 0) - (b.rarity == '太初' ? 1 : 0))
|
||||
res.push(
|
||||
undefined,
|
||||
undefined,
|
||||
...remind.filter((a) => a.itemDetailType == '项链'),
|
||||
undefined,
|
||||
...remind.filter((a) => a.itemDetailType == '手镯'),
|
||||
undefined,
|
||||
...remind.filter((a) => a.itemDetailType == '戒指'),
|
||||
...Array(Math.max(0, rowCount - remind.length - 4)).fill(undefined),
|
||||
)
|
||||
return res
|
||||
})
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (!curRarity.value) return
|
||||
if (curRarity.value == '太初') return
|
||||
suitEquipments.value
|
||||
.filter((a) => !!a && a.rarity == curRarity.value)
|
||||
.splice(0, 11)
|
||||
.forEach((a) => {
|
||||
configStore.chooseEqu(a)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(equip, _) in equs" :key="_">
|
||||
<EquipmentIcon
|
||||
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
|
||||
:equipment="equip"
|
||||
@click="configStore.chooseEqu(equip)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const equs = computed(() => {
|
||||
const items = infoStore.equips.filter(
|
||||
(equip) => equip.categorize === '通宝装备' || equip.categorize === '稀有装备 套装',
|
||||
)
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
const raritiyList = ['稀有', '神器', '传说', '史诗']
|
||||
raritiyList.forEach((rarity) => {
|
||||
const equ: (IEquipment | undefined)[] = items.filter((a) => a.rarity === rarity)
|
||||
equ.splice(5, 0, undefined)
|
||||
equ.splice(9, 0, undefined)
|
||||
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
|
||||
})
|
||||
return res
|
||||
})
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- 每排总计6+4+4,共计14个排列 -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
|
||||
<template v-for="(weapon, _) in weapons" :key="_">
|
||||
<EquipmentIcon
|
||||
:equipment="weapon"
|
||||
:inactive="configStore.config.equips['武器']?.id != weapon?.id"
|
||||
@click="configStore.chooseEqu(weapon)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rowCount?: number
|
||||
}>()
|
||||
|
||||
const rowCount = Math.max(props.rowCount ?? 0, 14)
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const weapons = computed(() => {
|
||||
const items = infoStore.equips.filter(
|
||||
(equip) => equip.itemType === '武器' && infoStore.infos?.weapons.includes(equip.itemDetailType),
|
||||
)
|
||||
const res: (IEquipment | undefined)[] = []
|
||||
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
|
||||
const weapon = items.filter((item) => item.itemDetailType === type)
|
||||
res.push(
|
||||
...weapon.filter((item) => item.categorize === '制式武器'),
|
||||
undefined,
|
||||
...weapon.filter((item) => item.categorize === '传世武器'),
|
||||
)
|
||||
const remind = rowCount - (res.length % rowCount)
|
||||
res.push(...Array(remind).fill(undefined))
|
||||
})
|
||||
return res
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
import { rarityClass } from '@/utils'
|
||||
import increaseIcon from '@/assets/img/icon/increase.png'
|
||||
import maxIcon from '@/assets/img/icon/max.png'
|
||||
import minIcon from '@/assets/img/icon/min.png'
|
||||
import reduceIcon from '@/assets/img/icon/reduce.png'
|
||||
|
||||
export type EquipmentLevelChange = 'preRarity' | 'nextRarity' | 'preLv' | 'nextLv'
|
||||
|
||||
const props = defineProps<{
|
||||
rarity: string
|
||||
level: number
|
||||
rarityList: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [direction: EquipmentLevelChange]
|
||||
}>()
|
||||
|
||||
const rarityIndex = computed(() => props.rarityList.findIndex((rarity) => rarity === props.rarity))
|
||||
const atFirstRarity = computed(() => rarityIndex.value <= 0)
|
||||
const atLastRarity = computed(() => rarityIndex.value >= props.rarityList.length - 1)
|
||||
|
||||
function formatLevel(level: number, rarity: string) {
|
||||
if (rarity === '太初') return ''
|
||||
const greekNumerals = ['Ι', 'ΙΙ', 'ΙΙΙ', 'ΙV', 'V', 'VΙ', 'VΙΙ', 'VΙΙΙ', 'ΙX', 'X']
|
||||
return greekNumerals[level - 1] || level.toString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="equipment-level-controls" aria-label="装备等级">
|
||||
<UiButton
|
||||
size="icon"
|
||||
title="降低品质"
|
||||
aria-label="降低品质"
|
||||
:disabled="atFirstRarity"
|
||||
@click="emit('change', 'preRarity')"
|
||||
>
|
||||
<img data-icon class="equipment-level-controls__icon" :src="minIcon" alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
title="降低等级"
|
||||
aria-label="降低等级"
|
||||
:disabled="atFirstRarity && level <= 1"
|
||||
@click="emit('change', 'preLv')"
|
||||
>
|
||||
<img data-icon class="equipment-level-controls__icon" :src="reduceIcon" alt="" />
|
||||
</UiButton>
|
||||
|
||||
<output class="equipment-level-controls__value" :class="rarityClass(rarity)">
|
||||
<span>{{ rarity }}</span>
|
||||
<span>{{ formatLevel(level, rarity) }}</span>
|
||||
</output>
|
||||
|
||||
<UiButton
|
||||
size="icon"
|
||||
title="提升等级"
|
||||
aria-label="提升等级"
|
||||
:disabled="atLastRarity"
|
||||
@click="emit('change', 'nextLv')"
|
||||
>
|
||||
<img data-icon class="equipment-level-controls__icon" :src="increaseIcon" alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
title="提升品质"
|
||||
aria-label="提升品质"
|
||||
:disabled="atLastRarity"
|
||||
@click="emit('change', 'nextRarity')"
|
||||
>
|
||||
<img data-icon class="equipment-level-controls__icon" :src="maxIcon" alt="" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.equipment-level-controls {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ui-autocomplete-trigger-gap);
|
||||
padding: var(--ui-disclosure-content-padding-block);
|
||||
background: var(--ui-settings-row-alt);
|
||||
}
|
||||
|
||||
.equipment-level-controls__value {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: var(--ui-control-height-default);
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ui-control-gap);
|
||||
padding-inline: var(--ui-control-padding-inline-compact);
|
||||
border: 1px solid var(--ui-disclosure-border);
|
||||
background: var(--ui-field-background);
|
||||
line-height: var(--ui-control-line-height);
|
||||
}
|
||||
|
||||
.equipment-level-controls__icon {
|
||||
width: var(--ui-badge-height);
|
||||
height: var(--ui-badge-height);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:deep(.ui-button:disabled) .equipment-level-controls__icon {
|
||||
filter: grayscale(1);
|
||||
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
SelectRoot,
|
||||
SelectValue,
|
||||
UiSelectContent,
|
||||
UiSelectItem,
|
||||
UiSelectTrigger,
|
||||
} from '@/components/ui/select'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const infoStore = useInfoStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 w-250px p-2">
|
||||
<div v-for="option in info.options" :key="option.id" class="flex items-center gap-2">
|
||||
<div class="w-60px text-end">{{ option.name }}</div>
|
||||
<calc-select
|
||||
class="flex-1 !h-20px"
|
||||
:appendTo="dialogRef"
|
||||
v-model="config.config.options[option.id.toString()]"
|
||||
>
|
||||
<calc-option v-for="(item, index) in option.options" :key="index" :value="index">
|
||||
{{ item }}
|
||||
</calc-option>
|
||||
</calc-select>
|
||||
</div>
|
||||
</div>
|
||||
<form class="equipment-options" @submit.prevent>
|
||||
<label v-for="option in infoStore.options" :key="option.id" class="equipment-options__row">
|
||||
<span>{{ option.name }}</span>
|
||||
<SelectRoot v-model="configStore.config.options[option.id.toString()]">
|
||||
<UiSelectTrigger :aria-label="option.name">
|
||||
<SelectValue placeholder="请选择" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="(item, index) in option.options" :key="index" :value="index">
|
||||
{{ item }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</SelectRoot>
|
||||
</label>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
const dialogRef = inject('dialogRef', ref(null))
|
||||
const config = useConfigStore()
|
||||
const info = useInfoStore()
|
||||
</script>
|
||||
<style scoped>
|
||||
.equipment-options {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: var(--ui-control-gap);
|
||||
}
|
||||
|
||||
.equipment-options__row {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(var(--ui-settings-row-min-block-size), auto) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
color: var(--ui-settings-row-foreground);
|
||||
}
|
||||
|
||||
.equipment-options__row > span {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,7 +164,7 @@ export const formatEqu = (total: IEquipment[], subweapons?: string[], curSuit?:
|
||||
equs: treasures,
|
||||
},
|
||||
{
|
||||
name: `${curSuit} 套装`,
|
||||
name: curSuit ? `${curSuit} 套装` : '套装',
|
||||
equs: suits,
|
||||
},
|
||||
{
|
||||
@@ -270,7 +270,7 @@ export const formatStone = (total: IEquipment[], curSuit?: string) => {
|
||||
// rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined))
|
||||
return [
|
||||
{
|
||||
name: `${curSuit} 套装`,
|
||||
name: curSuit ? `${curSuit} 套装` : '套装',
|
||||
equs: suits,
|
||||
},
|
||||
{
|
||||
@@ -315,11 +315,11 @@ export const formatOath = (
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: `${curSuit?.name} 誓约`,
|
||||
name: curSuit?.name ? `${curSuit.name} 誓约` : '誓约',
|
||||
equs: oath,
|
||||
},
|
||||
{
|
||||
name: `${curSuit?.name} 星蕴石`,
|
||||
name: curSuit?.name ? `${curSuit.name} 星蕴石` : '星蕴石',
|
||||
equs: primer,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, ref, renderList } from 'vue'
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import type { IEnchantingInfo } from '@/api/info/type'
|
||||
import { useInfoStore, useConfigStore } from '@/stores'
|
||||
import ClothList from '@/components/dnf/Avatar/List/index.vue'
|
||||
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
|
||||
export default defineComponent({
|
||||
name: 'Clothes',
|
||||
setup() {
|
||||
@@ -73,6 +74,25 @@ export default defineComponent({
|
||||
// 属性
|
||||
const option = currentInfo<string | number>('option')
|
||||
|
||||
const enchantOptions = computed<ForgeSelectOption[]>(() => [
|
||||
{ label: '无', value: 0 },
|
||||
...(enchant_list.value ?? []).map((item) => ({ label: item.detail, value: item.id })),
|
||||
])
|
||||
|
||||
const avatarGradeOptions = computed<ForgeSelectOption[]>(() =>
|
||||
(basicInfoStore.avatars[part.value] ?? []).map((item) => ({
|
||||
label: item.suit ?? '',
|
||||
value: item.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const avatarPropertyOptions = computed<ForgeSelectOption[]>(() => [
|
||||
{ label: '无', value: 0 },
|
||||
...(
|
||||
basicInfoStore.avatars[part.value]?.find((avatar) => avatar.id == id.value)?.options ?? []
|
||||
).map((item) => ({ label: item, value: item })),
|
||||
])
|
||||
|
||||
/**
|
||||
* 同步徽章1到徽章2
|
||||
* @param val
|
||||
@@ -84,47 +104,41 @@ export default defineComponent({
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="h-120px flex justify-center items-center">
|
||||
<div class="forge-preview">
|
||||
<ClothList v-model:part={part.value} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap equ-profile">
|
||||
<div class="equ-profile-item">
|
||||
<div class="mr-10px row-name">当前部位</div>
|
||||
<div class="row-name">当前部位</div>
|
||||
<span> {part.value}</span>
|
||||
</div>
|
||||
{has_enchant.value ? (
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">{part.value == '宠物' ? '附魔' : '属性'}</div>
|
||||
<calc-select v-model={enchant.value} class="flex-1 !h-20px">
|
||||
<calc-option value={0}>无</calc-option>
|
||||
{renderList(enchant_list.value ?? [], (item) => (
|
||||
<calc-option value={item.id}>{item.detail}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={enchant.value}
|
||||
label={part.value == '宠物' ? '宠物附魔' : '装扮属性'}
|
||||
options={enchantOptions.value}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">品级</div>
|
||||
<calc-select v-model={id.value} class="flex-1 !h-20px">
|
||||
{renderList(basicInfoStore.avatars[part.value] ?? [], (item) => (
|
||||
<calc-option value={item.id}>{item.suit}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={id.value}
|
||||
label="装扮品级"
|
||||
options={avatarGradeOptions.value}
|
||||
/>
|
||||
</div>
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">属性</div>
|
||||
<calc-select v-model={option.value} class="flex-1 !h-20px">
|
||||
<calc-option value={0}>无</calc-option>
|
||||
{renderList(
|
||||
basicInfoStore.avatars[part.value].find((a) => a.id == id.value)?.options ??
|
||||
[],
|
||||
(item) => (
|
||||
<calc-option value={item}>{item}</calc-option>
|
||||
),
|
||||
)}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={option.value}
|
||||
label="装扮属性"
|
||||
options={avatarPropertyOptions.value}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
<template>
|
||||
<div class="h-179px flex justify-center items-center">
|
||||
<div class="forge-preview">
|
||||
<EmblemList :withBg="true" v-model:part="part" :detail="configStore.config.emblems" />
|
||||
</div>
|
||||
<div class="flex flex-wrap equ-profile">
|
||||
<div class="equ-profile-item">
|
||||
<div class="mr-10px row-name">当前槽位</div>
|
||||
<div class="row-name">当前槽位</div>
|
||||
<span>{{ part }}</span>
|
||||
</div>
|
||||
<div class="equ-profile-item" v-if="part != '白金'">
|
||||
<div class="row-name">等级</div>
|
||||
<calc-select v-model="lv" class="flex-1 !h-20px">
|
||||
<template v-for="(i, index) in lvs" :key="index">
|
||||
<calc-option :value="index">{{ i }}</calc-option>
|
||||
</template>
|
||||
</calc-select>
|
||||
<ForgeSelect v-model="lv" label="徽章等级" :options="levelOptions" />
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-basis-50px text-center mr-10px">徽章</div>
|
||||
<div class="flex-1 grid gap-4px" :style="emblemGridStyle">
|
||||
<div
|
||||
v-for="(emblem, index) in emblemMaxCount"
|
||||
:style="emblemItemStyle(index)"
|
||||
class="flex"
|
||||
:key="index"
|
||||
>
|
||||
<calc-select v-model="emblems[index]" class="min-w-0 flex-1 !h-20px">
|
||||
<template v-for="(i, index) in emblemOptions" :key="index">
|
||||
<calc-option :value="i.id">[{{ i.rarity }}]{{ i.value }}</calc-option>
|
||||
</template>
|
||||
</calc-select>
|
||||
<div class="equ-profile-item forge-emblem-row">
|
||||
<div class="row-name">徽章</div>
|
||||
<div class="forge-emblem-grid">
|
||||
<div v-for="(_, index) in emblemMaxCount" :style="emblemItemStyle(index)" :key="index">
|
||||
<ForgeSelect
|
||||
v-model="emblems[index]"
|
||||
:label="`徽章槽位 ${index + 1}`"
|
||||
:options="emblemSelectOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,6 +29,7 @@
|
||||
<script lang="ts" setup name="Emblems">
|
||||
import EmblemList from '@/components/dnf/Emblem/List/index.vue'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
|
||||
|
||||
const part = ref('彩色')
|
||||
|
||||
@@ -63,6 +55,8 @@ const lvs = [
|
||||
'史诗Lv.1',
|
||||
]
|
||||
|
||||
const levelOptions: ForgeSelectOption[] = lvs.map((label, value) => ({ label, value }))
|
||||
|
||||
const emblemOptions = computed(() => {
|
||||
if (part.value == '彩色') {
|
||||
return infoStore.emblems.filter((a) => a.type != '白金').sort((a, b) => b.fame - a.fame)
|
||||
@@ -81,6 +75,13 @@ const emblemOptions = computed(() => {
|
||||
.sort((a, b) => b.fame - a.fame)
|
||||
})
|
||||
|
||||
const emblemSelectOptions = computed<ForgeSelectOption[]>(() =>
|
||||
emblemOptions.value.map((emblem) => ({
|
||||
label: `[${emblem.rarity}]${emblem.value}`,
|
||||
value: emblem.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const emblemMaxCount = computed(() => {
|
||||
if (part.value == '彩色') {
|
||||
return 6
|
||||
@@ -91,12 +92,6 @@ const emblemMaxCount = computed(() => {
|
||||
return 4
|
||||
})
|
||||
|
||||
const emblemGridStyle = computed(() => {
|
||||
return {
|
||||
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||
}
|
||||
})
|
||||
|
||||
const emblemItemStyle = (index: number) => {
|
||||
const count = emblemMaxCount.value
|
||||
const remainder = count % 2
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, ref, renderList } from 'vue'
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import type { IEnchantingInfo } from '@/api/info/type'
|
||||
import { useInfoStore, useConfigStore } from '@/stores'
|
||||
import EquList from '@/components/dnf/Equipment/List/index.vue'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import { UiCheckbox } from '@/components/ui/checkbox'
|
||||
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
|
||||
import ForgeSlider from './ForgeSlider.vue'
|
||||
export default defineComponent({
|
||||
name: 'Equip',
|
||||
setup() {
|
||||
@@ -121,11 +123,32 @@ export default defineComponent({
|
||||
//精度
|
||||
const precision = currentInfo<number>('precision', 0)
|
||||
|
||||
const reinforceTypeOptions: ForgeSelectOption[] = [
|
||||
{ label: '强化', value: 0 },
|
||||
{ label: '增幅', value: 1 },
|
||||
]
|
||||
const reinforceOptions: ForgeSelectOption[] = Array.from({ length: 31 }, (_, value) => ({
|
||||
label: String(value),
|
||||
value,
|
||||
}))
|
||||
const refineOptions: ForgeSelectOption[] = Array.from({ length: 9 }, (_, value) => ({
|
||||
label: `锻造+ ${value}`,
|
||||
value,
|
||||
}))
|
||||
const adaptationOptions: ForgeSelectOption[] = Array.from({ length: 4 }, (_, value) => ({
|
||||
label: String(value),
|
||||
value,
|
||||
}))
|
||||
const enchantOptions = computed<ForgeSelectOption[]>(() => [
|
||||
{ label: '无', value: '' },
|
||||
...(enchant_list.value ?? []).map((item) => ({ label: item.detail, value: item.id })),
|
||||
])
|
||||
|
||||
// 武器融合
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="h-150px flex justify-center items-center">
|
||||
<div class="forge-preview">
|
||||
<EquList
|
||||
v-model:part={part.value}
|
||||
withBg={false}
|
||||
@@ -136,108 +159,86 @@ export default defineComponent({
|
||||
|
||||
<div class="flex flex-wrap equ-profile">
|
||||
<div class="equ-profile-item">
|
||||
<div class="mr-10px row-name">当前部位</div>
|
||||
<div class="row-name">当前部位</div>
|
||||
<span> {part.value}</span>
|
||||
{can_upgrade.value ? (
|
||||
<calc-checkbox
|
||||
style="margin-left:auto"
|
||||
v-model={global_change.value}
|
||||
label="全局修改"
|
||||
></calc-checkbox>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<label class="forge-checkbox" style="margin-left:auto">
|
||||
<UiCheckbox v-model={global_change.value} aria-label="全局修改" />
|
||||
<span>全局修改</span>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
{part.value == '副武器' ? (
|
||||
<>
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">增幅</div>
|
||||
<calc-select v-model={reinforce_type.value} class="flex-1 !h-20px">
|
||||
<calc-option value={0}>强化</calc-option>
|
||||
<calc-option value={1}>增幅</calc-option>
|
||||
</calc-select>
|
||||
<calc-select v-model={reinforce.value} class="flex-1 !h-20px">
|
||||
{renderList(31, (i) => (
|
||||
<calc-option value={i - 1}>{i - 1}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
</div>
|
||||
</>
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">增幅</div>
|
||||
<ForgeSelect
|
||||
v-model={reinforce_type.value}
|
||||
label="副武器强化类型"
|
||||
options={reinforceTypeOptions}
|
||||
/>
|
||||
<ForgeSelect
|
||||
v-model={reinforce.value}
|
||||
label="副武器强化等级"
|
||||
options={reinforceOptions}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{can_upgrade.value ? (
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">增幅</div>
|
||||
<calc-select v-model={reinforce_type.value} class="flex-1 !h-20px">
|
||||
<calc-option value={0}>强化</calc-option>
|
||||
<calc-option value={1}>增幅</calc-option>
|
||||
</calc-select>
|
||||
<calc-select v-model={reinforce.value} class="flex-1 !h-20px">
|
||||
{renderList(31, (i) => (
|
||||
<calc-option value={i - 1}>{i - 1}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={reinforce_type.value}
|
||||
label={`${part.value}强化类型`}
|
||||
options={reinforceTypeOptions}
|
||||
/>
|
||||
<ForgeSelect
|
||||
v-model={reinforce.value}
|
||||
label={`${part.value}强化等级`}
|
||||
options={reinforceOptions}
|
||||
/>
|
||||
{part.value == '武器' && (
|
||||
<calc-select v-model={refine.value} class="flex-1 !h-20px">
|
||||
{renderList(9, (i) => (
|
||||
<calc-option value={i - 1}>锻造+ {i - 1}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={refine.value}
|
||||
label="武器锻造等级"
|
||||
options={refineOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">附魔</div>
|
||||
<calc-select v-model={enchant.value} class="flex-1 !h-20px">
|
||||
<calc-option value={''}>无</calc-option>
|
||||
{renderList(enchant_list.value ?? [], (item) => (
|
||||
<calc-option value={item.id}>{item.detail}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={enchant.value}
|
||||
label={`${part.value}附魔`}
|
||||
options={enchantOptions.value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{can_upgrade.value ? (
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">调适</div>
|
||||
{/* <calc-slider
|
||||
showValue={true}
|
||||
showPercent={false}
|
||||
v-model={adaptation.value}
|
||||
min={0}
|
||||
max={3}
|
||||
step={1}
|
||||
class="flex-1 !h-20px"
|
||||
/> */}
|
||||
<calc-select v-model={adaptation.value} class="flex-1 !h-20px">
|
||||
{renderList(4, (i) => (
|
||||
<calc-option value={i - 1}>{i - 1}</calc-option>
|
||||
))}
|
||||
</calc-select>
|
||||
<ForgeSelect
|
||||
v-model={adaptation.value}
|
||||
label={`${part.value}调适`}
|
||||
options={adaptationOptions}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
) : null}
|
||||
{can_set_secret.value ? (
|
||||
<>
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">精度</div>
|
||||
<calc-slider
|
||||
showPercent={true}
|
||||
showValue={false}
|
||||
v-model={precision.value}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
class="flex-1 !h-20px"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<div class="equ-profile-item">
|
||||
<div class="row-name">精度</div>
|
||||
<ForgeSlider
|
||||
modelValue={precision.value}
|
||||
onUpdate:modelValue={(value) => (precision.value = value)}
|
||||
label={`${part.value}秘宝精度`}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||