change responsive]
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<CollapsePanel :key="activeKey">
|
||||
<template #header>
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span>{{ props.title }}</span>
|
||||
<BaseIcon :class="[{ rotate: props.actives.includes(props.activeKey) }]" icon="vuesax-linear:arrow-left-1" size="17" />
|
||||
</div>
|
||||
<div :class="[{ open: props.actives.includes(props.activeKey) }]"></div>
|
||||
</div>
|
||||
</template>
|
||||
<slot name="content"></slot>
|
||||
</CollapsePanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CollapsePanel } from 'ant-design-vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
activeKey: string | number
|
||||
actives: string[] | number[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.open {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #E5E7EB;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
transform: rotate(-90deg);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
|
||||
direction: rtl !important;
|
||||
justify-content: space-between !important;
|
||||
background-color: #fff !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ant-collapse{
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
|
||||
padding: 12px 16px 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-if="!isMobileView"
|
||||
:key="`action-sheet-modal-${overlayKey}`"
|
||||
v-model:visible="visibleProxy"
|
||||
:title="title"
|
||||
:width="modalWidth"
|
||||
:loading="loading"
|
||||
custom-footer
|
||||
@close="handleCancel"
|
||||
@ok="$emit('ok')"
|
||||
>
|
||||
<slot />
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="showButton && (showCancel || showOk)"
|
||||
class="grid gap-3"
|
||||
:class="[!showOk || !showCancel ? 'grid-cols-1' : 'grid-cols-12']"
|
||||
>
|
||||
<AButton
|
||||
v-if="showCancel"
|
||||
type="primary"
|
||||
:class="[{ 'col-span-4': showOk }]"
|
||||
size="large"
|
||||
ghost
|
||||
block
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="showOk"
|
||||
type="primary"
|
||||
:class="[{ 'col-span-8': showCancel }]"
|
||||
size="large"
|
||||
block
|
||||
@click="$emit('ok')"
|
||||
>
|
||||
{{ okText }}
|
||||
</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</BasicModal>
|
||||
|
||||
<ADrawer
|
||||
v-else-if="isMobileView"
|
||||
:key="`action-sheet-drawer-${overlayKey}`"
|
||||
v-model:visible="visibleProxy"
|
||||
placement="bottom"
|
||||
:closable="false"
|
||||
:destroy-on-close="true"
|
||||
:height="drawerHeight"
|
||||
class="basic-action-sheet"
|
||||
>
|
||||
<div class="basic-action-sheet__container">
|
||||
<div class="flex cursor-pointer justify-center pt-2" @click="visibleProxy = false">
|
||||
<span class="w-12 rounded-sm border-t-2 border-t-solid border-t-gray-500"></span>
|
||||
</div>
|
||||
<div class="basic-action-sheet__content">
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
v-if="showButton && (showCancel || showOk)"
|
||||
class="basic-action-sheet__footer grid gap-3"
|
||||
:class="[!showOk || !showCancel ? 'grid-cols-1' : 'grid-cols-12']"
|
||||
>
|
||||
<AButton
|
||||
v-if="showCancel"
|
||||
type="primary"
|
||||
:class="[{ 'col-span-4': showOk }]"
|
||||
size="large"
|
||||
ghost
|
||||
block
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="showOk"
|
||||
type="primary"
|
||||
:class="[{ 'col-span-8': showCancel }]"
|
||||
size="large"
|
||||
block
|
||||
@click="$emit('ok')"
|
||||
>
|
||||
{{ okText }}
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import { ACTION_SHEET_HEIGHT } from '@/constants/actionSheet'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
|
||||
interface Props {
|
||||
visible?: boolean
|
||||
title?: string
|
||||
modalWidth?: string
|
||||
loading?: boolean
|
||||
drawerHeight?: string
|
||||
cancelText?: string
|
||||
showButton?: boolean
|
||||
showCancel?: boolean
|
||||
showOk?: boolean
|
||||
okText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
title: '',
|
||||
modalWidth: '36rem',
|
||||
loading: false,
|
||||
drawerHeight: ACTION_SHEET_HEIGHT,
|
||||
cancelText: 'بازگشت',
|
||||
showButton: false,
|
||||
showCancel: true,
|
||||
showOk: true,
|
||||
okText: 'ثبت'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'update:show', value: boolean): void
|
||||
(e: 'ok'): void
|
||||
(e: 'cancel'): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const isMobileView = computed(() => isMobile.value)
|
||||
const overlayKey = computed(() => (isMobileView.value ? 'drawer' : 'modal'))
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (value: boolean) => {
|
||||
emit('update:visible', value)
|
||||
emit('update:show', value)
|
||||
}
|
||||
})
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
emit('close')
|
||||
visibleProxy.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.basic-action-sheet {
|
||||
.ant-drawer-content {
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.basic-action-sheet__container {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.basic-action-sheet__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.basic-action-sheet__footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
background: #fff;
|
||||
padding: 8px 16px calc(16px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,20 @@
|
||||
<template>
|
||||
<a-card class="m-5 rounded-xl md:h-[91vh]">
|
||||
<slot></slot>
|
||||
</a-card>
|
||||
<div class="box-border flex h-full min-h-0 min-w-0 w-full max-w-full flex-col overflow-hidden p-3 md:p-5">
|
||||
<a-card class="basic-main-card flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl">
|
||||
<slot></slot>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script></script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.basic-main-card :deep(.ant-card-body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- {{ props.menu }}-->
|
||||
<AMenu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:style="{ width: props.width }"
|
||||
:mode="props.mode"
|
||||
:theme="props.theme"
|
||||
>
|
||||
<!-- <a-menu-item key="1"> Navigation One </a-menu-item>-->
|
||||
<!-- {{ props.menu }}-->
|
||||
<BasicMenuItem :menu="props.menu" />
|
||||
</AMenu>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<nav class="mobile-bottom-nav md:hidden" aria-label="منوی اصلی">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="String(item.name)"
|
||||
type="button"
|
||||
class="mobile-bottom-nav__item"
|
||||
:class="{ 'mobile-bottom-nav__item--active': isRouteActive(item) }"
|
||||
@click="navigateTo(item)"
|
||||
>
|
||||
<span class="mobile-bottom-nav__icon-wrap">
|
||||
<BaseIcon
|
||||
v-if="item.meta?.icon"
|
||||
:icon="item.meta.icon as string"
|
||||
:size="menuIconSize"
|
||||
/>
|
||||
</span>
|
||||
<span class="mobile-bottom-nav__label">{{ item.meta?.title ?? item.name }}</span>
|
||||
</button>
|
||||
<BasicMobileOverflowMenu :items="overflowMenuItems" @select="navigateTo" />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BaseIcon from '@core/components/BaseIcon'
|
||||
import { menuIconSize } from '@root/config'
|
||||
import { useRouterStore } from '@core/stores/router'
|
||||
import BasicMobileOverflowMenu from '@/components/BasicMobileOverflowMenu.vue'
|
||||
import { sortBy } from 'lodash'
|
||||
import { computed } from 'vue'
|
||||
import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const BOTTOM_NAV_PRIORITY: (string | symbol)[] = ['dashboard', 'orders', 'accounting', 'customers']
|
||||
|
||||
const router = useRouter()
|
||||
const currentRoute = useRoute()
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
const allMenuItems = computed(() =>
|
||||
sortBy(
|
||||
routerStore.routes.filter((item) => !item.meta?.hideMenu),
|
||||
(item) => item.meta?.order
|
||||
)
|
||||
)
|
||||
|
||||
const navItems = computed(() => {
|
||||
const items: RouteRecordRaw[] = []
|
||||
|
||||
for (const name of BOTTOM_NAV_PRIORITY) {
|
||||
const route = allMenuItems.value.find((item) => item.name === name)
|
||||
if (route) items.push(route)
|
||||
}
|
||||
|
||||
for (const route of allMenuItems.value) {
|
||||
if (items.length >= 4) break
|
||||
if (!items.some((item) => item.name === route.name)) {
|
||||
items.push(route)
|
||||
}
|
||||
}
|
||||
|
||||
return items.slice(0, 4)
|
||||
})
|
||||
|
||||
const navItemNames = computed(() => new Set(navItems.value.map((item) => item.name)))
|
||||
|
||||
const overflowMenuItems = computed(() =>
|
||||
allMenuItems.value.filter((item) => !navItemNames.value.has(item.name))
|
||||
)
|
||||
|
||||
function isRouteActive(route: RouteRecordRaw): boolean {
|
||||
if (currentRoute.name === route.name) return true
|
||||
|
||||
function checkChildren(children?: RouteRecordRaw[]): boolean {
|
||||
if (!children?.length) return false
|
||||
for (const child of children) {
|
||||
if (child.name === currentRoute.name) return true
|
||||
if (checkChildren(child.children)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return checkChildren(route.children)
|
||||
}
|
||||
|
||||
function navigateTo(item: RouteRecordRaw) {
|
||||
const redirect = item.redirect
|
||||
if (redirect && typeof redirect === 'object' && 'name' in redirect) {
|
||||
router.push({ name: redirect.name as RouteRecordName })
|
||||
return
|
||||
}
|
||||
router.push({ name: item.name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mobile-bottom-nav {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 4px;
|
||||
padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 6px 4px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #595959;
|
||||
|
||||
&--active {
|
||||
color: #1677ff;
|
||||
|
||||
.mobile-bottom-nav__icon-wrap {
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
color: #292d32;
|
||||
|
||||
:deep(svg path) {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,18 @@
|
||||
<template>
|
||||
<div theme="light" :class="[prefixCls]" class="md:hidden">
|
||||
<div class="flex items-center justify-center gap-2 relative">
|
||||
<BaseIcon
|
||||
icon="vuesax-linear:menu"
|
||||
class="p-3 absolute right-2 top-1"
|
||||
@click="showDrawer"
|
||||
/>
|
||||
<img :src="getConfigApp('avatar')" class="w-4rem mr-2 my-5" />
|
||||
<div class="absolute left-2 top-3">
|
||||
<div class="flex items-center w-full justify-center"></div>
|
||||
<div class="mobile-header-bar">
|
||||
<span class="mobile-header-bar__name">{{ displayName }}</span>
|
||||
<div class="mobile-header-bar__logo">
|
||||
<BasicMobileHeaderLogo />
|
||||
</div>
|
||||
<div class="mobile-header-bar__actions">
|
||||
<button type="button" class="mobile-header-bar__icon" aria-label="رفرش صفحه" @click="handleRefresh">
|
||||
<Icon icon="vuesax-linear:refresh" :size="26" />
|
||||
</button>
|
||||
<ADropdown placement="bottomLeft">
|
||||
<a class="ant-dropdown-link" @click.prevent>
|
||||
<!-- Hover me-->
|
||||
<div class="flex items-center gap-1 text-gray-400">
|
||||
<AAvatar :src="placeholderM" />
|
||||
<Icon icon="vuesax-linear:arrow-down" size="15" />
|
||||
</div>
|
||||
</a>
|
||||
<button type="button" class="mobile-header-bar__icon" aria-label="پروفایل">
|
||||
<Icon icon="vuesax-linear:profile-circle" :size="26" />
|
||||
</button>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem>
|
||||
@@ -41,98 +37,53 @@
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
<BaseButton
|
||||
iconSize="20"
|
||||
icon="vuesax-linear:setting"
|
||||
type="text"
|
||||
v-if="layoutStore.showSettingsIcon"
|
||||
@click="handleShowSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BasicModal
|
||||
v-model:visible="myInfoVisible"
|
||||
width="40rem"
|
||||
title="اطلاعات حساب کاربری"
|
||||
@ok="ok"
|
||||
:loading="loading"
|
||||
@close="myInfoVisible = false"
|
||||
>
|
||||
<a-form :model="form" ref="formRef" autocomplete="off">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<a-form-item label="نام" name="fname" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.fname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.lname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="شماره موبایل" name="mobile" :rules="inputLenRules(11)">
|
||||
<BasicInput v-model:value="form.mobile" type="number" />
|
||||
</a-form-item>
|
||||
<a-form-item label="رمز کاربری" name="password">
|
||||
<BasicInput type="password" v-model:value="form.password" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</div>
|
||||
|
||||
<BasicModal
|
||||
v-model:visible="myInfoVisible"
|
||||
width="40rem"
|
||||
title="اطلاعات حساب کاربری"
|
||||
@ok="ok"
|
||||
:loading="loading"
|
||||
@close="myInfoVisible = false"
|
||||
>
|
||||
<a-form :model="form" ref="formRef" autocomplete="off">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<a-form-item label="نام" name="fname" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.fname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.lname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="شماره موبایل" name="mobile" :rules="inputLenRules(11)">
|
||||
<BasicInput v-model:value="form.mobile" type="number" />
|
||||
</a-form-item>
|
||||
<a-form-item label="رمز کاربری" name="password">
|
||||
<BasicInput type="password" v-model:value="form.password" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</div>
|
||||
<ADrawer height="460px" :placement="placement" v-model:visible="open" :closable="false" @close="onClose">
|
||||
<AMenu v-model:selectedKeys="selectedKeys" mode="vertical">
|
||||
<template v-for="item in menu">
|
||||
<BasicSubMenu
|
||||
v-if="!item.meta?.hideChildren && item.children && item.children.length > 0"
|
||||
:menu-item="item"
|
||||
:key="item.name"
|
||||
:level="1"
|
||||
@click="onClose"
|
||||
/>
|
||||
<BaseMenuItem v-else :item="item" :key="item.name" @click="onClose" />
|
||||
</template>
|
||||
</AMenu>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import Logo from '@/assets/img/saminLogo.png'
|
||||
import type { DrawerProps } from 'ant-design-vue'
|
||||
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
||||
|
||||
const placement = ref<DrawerProps['placement']>('bottom')
|
||||
import { usePrefix } from '@core/composable/usePrefix'
|
||||
import { useLayoutStore } from '@core/stores/layout'
|
||||
import BasicMenu from '@core/layouts/composnts/BasicMenu.vue'
|
||||
import placeholderM from '@core/assets/img/avatars/placeholder-m.jpg'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { useSettingsStore } from '@core/stores/settings'
|
||||
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
||||
import BasicMobileHeaderLogo from '@/components/BasicMobileHeaderLogo.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import type { User } from '@/model/user'
|
||||
import { createUser, updateMyInformation, updateUser } from '@/api/user'
|
||||
import { updateMyInformation } from '@/api/user'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { cloneDeep, sortBy } from 'lodash'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
|
||||
import { useRouterStore } from '@core/stores/router'
|
||||
import BasicSubMenu from '@core/layouts/composnts/BasicSubMenu.vue'
|
||||
import BaseMenuItem from '@core/layouts/composnts/BaseMenuItem.vue'
|
||||
import {getConfigApp} from "@/utils/config";
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const { prefixCls } = usePrefix('basic-header')
|
||||
const layoutStore = useLayoutStore()
|
||||
const store = useStaticApiListsStore()
|
||||
const userStore = useUserStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const currentRoute = useRoute()
|
||||
const selectedKeys = ref<(RouteRecordName | undefined)[]>([])
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
//my info modal
|
||||
const loading = ref<boolean>(false)
|
||||
const myInfoVisible = ref<boolean>(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -150,45 +101,17 @@ const form = ref<User>({
|
||||
user_type: undefined,
|
||||
permission: []
|
||||
})
|
||||
|
||||
const displayName = computed(() => {
|
||||
const info = userStore.userInfo
|
||||
if (info?.name) return info.name
|
||||
return [info?.fname, info?.lname].filter(Boolean).join(' ') || 'کاربر'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
form.value = cloneDeep(userStore?.userInfo) as User
|
||||
if (width.value <= 426) {
|
||||
layoutStore.layout = 'side'
|
||||
layoutStore.sideMenu.mode = 'vertical'
|
||||
layoutStore.sideMenu.collapsed = false
|
||||
console.log(layoutStore.layout + width.value)
|
||||
}
|
||||
const path: (RouteRecordName | undefined)[] = []
|
||||
|
||||
function walkInRoutes(routes: RouteRecordRaw[]) {
|
||||
for (const route of routes) {
|
||||
if (route.name == currentRoute.name) {
|
||||
path.push(route.name)
|
||||
return true
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
path.push(route.name)
|
||||
const res = walkInRoutes(route.children)
|
||||
if (!res) {
|
||||
path.pop()
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
walkInRoutes(routerStore.routes)
|
||||
selectedKeys.value = path
|
||||
})
|
||||
const menu: ComputedRef<RouteRecordRaw[]> = computed(() => {
|
||||
return sortBy(routerStore.routes, (item) => {
|
||||
return item.meta?.order
|
||||
}).filter((item) => !item.meta?.hideMenu)
|
||||
})
|
||||
|
||||
//submit user
|
||||
async function ok() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
loading.value = true
|
||||
@@ -198,7 +121,6 @@ async function ok() {
|
||||
message.success('اطلاعات با موفقیت ویرایش شد')
|
||||
myInfoVisible.value = false
|
||||
} catch (e) {
|
||||
// message.error('خطا در بارگذاری اطلاعات')
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -206,30 +128,79 @@ async function ok() {
|
||||
})
|
||||
}
|
||||
|
||||
//const temp = 12
|
||||
function handleCollapseSideMenu() {
|
||||
layoutStore.sideMenu.collapsed = !layoutStore.sideMenu.collapsed
|
||||
}
|
||||
|
||||
function handleShowSettings() {
|
||||
settingsStore.showSettingsDrawer = !settingsStore.showSettingsDrawer
|
||||
}
|
||||
|
||||
function clearLocalStorage() {
|
||||
localStorage.clear()
|
||||
location.reload()
|
||||
}
|
||||
|
||||
const open = ref<boolean>(false)
|
||||
|
||||
const showDrawer = () => {
|
||||
open.value = true
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
open.value = false
|
||||
console.log(open.value)
|
||||
function handleRefresh() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
<style scoped lang="less">
|
||||
@prefix: ~'@{prefixCls}-basic-header';
|
||||
|
||||
.@{prefix} {
|
||||
padding: 12px 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mobile-header-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.mobile-header-bar__name {
|
||||
max-width: 34%;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1f1f1f;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mobile-header-bar__logo {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mobile-header-bar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-header-bar__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #292d32;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<img :src="getConfigApp('avatar')" alt="لوگو" class="mobile-header-logo" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mobile-header-logo {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div ref="menuRef" class="mobile-overflow-menu">
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-overflow-menu__trigger"
|
||||
:class="{ 'mobile-overflow-menu__trigger--active': open }"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<span class="mobile-overflow-menu__icon-wrap">
|
||||
<BaseIcon icon="vuesax-linear:menu" :size="menuIconSize" />
|
||||
</span>
|
||||
<span class="mobile-overflow-menu__label">منو</span>
|
||||
</button>
|
||||
|
||||
<div v-if="open" class="mobile-overflow-menu__panel">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="String(item.name)"
|
||||
type="button"
|
||||
class="mobile-overflow-menu__panel-item"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<span class="mobile-overflow-menu__panel-label">{{ item.meta?.title ?? item.name }}</span>
|
||||
<BaseIcon
|
||||
v-if="item.meta?.icon"
|
||||
:icon="item.meta.icon as string"
|
||||
:size="menuIconSize"
|
||||
class="mobile-overflow-menu__panel-icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BaseIcon from '@core/components/BaseIcon'
|
||||
import { menuIconSize } from '@root/config'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
interface Props {
|
||||
items: RouteRecordRaw[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', item: RouteRecordRaw): void
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onClickOutside(menuRef, () => {
|
||||
open.value = false
|
||||
})
|
||||
|
||||
function toggleMenu() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
function handleSelect(item: RouteRecordRaw) {
|
||||
open.value = false
|
||||
emit('select', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mobile-overflow-menu {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 6px 4px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #595959;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__trigger--active {
|
||||
color: #1677ff;
|
||||
|
||||
.mobile-overflow-menu__icon-wrap {
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
color: #292d32;
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__panel {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 12px);
|
||||
left: 8px;
|
||||
min-width: 220px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
padding: 10px;
|
||||
z-index: 210;
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__panel-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 10px 8px;
|
||||
color: #262626;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
|
||||
&:active {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-overflow-menu__panel-icon {
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="py-2 pr-3 w-full h-full relative">
|
||||
<div class="text-black font-bold">{{ props.title }}</div>
|
||||
<div class="mt-2 overflow-y-auto p-1 h-[96%]">
|
||||
<div class="relative flex h-full min-h-0 min-w-0 w-full max-w-full flex-col overflow-hidden py-2 pr-3">
|
||||
<div class="flex shrink-0 min-w-0 items-center justify-between gap-2">
|
||||
<div class="truncate text-black font-bold">{{ props.title }}</div>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
<div class="mt-2 min-h-0 flex-1 overflow-hidden p-1">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,5 +16,3 @@ interface Prop {
|
||||
}
|
||||
const props = defineProps<Prop>()
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<article
|
||||
class="rounded-2xl border border-gray-200 bg-white shadow-sm"
|
||||
:class="[paddingClass, clickable ? 'cursor-pointer' : '']"
|
||||
@click="clickable ? $emit('click') : undefined"
|
||||
>
|
||||
<slot />
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
clickable?: boolean
|
||||
compact?: boolean
|
||||
}>(),
|
||||
{
|
||||
clickable: false,
|
||||
compact: true
|
||||
}
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const paddingClass = computed(() => (props.compact ? 'px-3 py-3' : 'p-4'))
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="min-w-0 h-full max-w-full overflow-x-hidden overflow-y-auto space-y-3 md:hidden">
|
||||
<div v-if="$slots.toolbar" class="flex">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
|
||||
<div v-if="showSearch" class="flex items-center gap-2">
|
||||
<a-input
|
||||
v-model:value="searchModel"
|
||||
:placeholder="searchPlaceholder"
|
||||
class="w-full flex-1"
|
||||
@keyup.enter="$emit('search')"
|
||||
>
|
||||
<template #prefix>
|
||||
<BaseIcon icon="vuesax-linear:search-normal" size="18" color="#9ca3af" />
|
||||
</template>
|
||||
</a-input>
|
||||
<BaseButton v-if="showRefresh" type="text" icon="vuesax-linear:refresh" iconSize="18" @click="$emit('refresh')" />
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading" tip="در حال دریافت اطلاعات...">
|
||||
<div class="space-y-2">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showEmpty"
|
||||
class="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-3 py-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
|
||||
<div v-if="hasMore" class="mt-3 flex justify-center">
|
||||
<BaseButton size="small" @click="$emit('loadMore')">بارگذاری بیشتر</BaseButton>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
loading: boolean
|
||||
hasMore: boolean
|
||||
emptyText?: string
|
||||
showEmpty?: boolean
|
||||
search?: string
|
||||
searchPlaceholder?: string
|
||||
showSearch?: boolean
|
||||
showRefresh?: boolean
|
||||
}>(),
|
||||
{
|
||||
emptyText: 'موردی یافت نشد',
|
||||
showEmpty: false,
|
||||
searchPlaceholder: 'جستجو...',
|
||||
showSearch: true,
|
||||
showRefresh: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search'): void
|
||||
(e: 'refresh'): void
|
||||
(e: 'loadMore'): void
|
||||
(e: 'update:search', value: string): void
|
||||
}>()
|
||||
|
||||
const searchModel = computed({
|
||||
get: () => props.search ?? '',
|
||||
set: (value: string) => emit('update:search', value)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex justify-between rounded-lg border px-3 py-2" :class="rowClass">
|
||||
<div class="text-xs" :class="labelClass">{{ label }}</div>
|
||||
<div class="truncate font-700 mr-1" :class="valueClass">
|
||||
<slot>{{ value }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
value?: string | number
|
||||
variant?: 'neutral' | 'success' | 'warning' | 'danger'
|
||||
fullWidth?: boolean
|
||||
valueClass?: string
|
||||
}>(),
|
||||
{
|
||||
variant: 'neutral',
|
||||
fullWidth: false
|
||||
}
|
||||
)
|
||||
|
||||
const rowClass = computed(() => {
|
||||
const span = props.fullWidth ? 'col-span-2' : ''
|
||||
const variants: Record<string, string> = {
|
||||
neutral: 'border-gray-200 bg-gray-50',
|
||||
success: 'border-green-200 bg-green-50',
|
||||
warning: 'border-amber-200 bg-amber-50',
|
||||
danger: 'border-red-200 bg-red-50'
|
||||
}
|
||||
return `${span} ${variants[props.variant]}`
|
||||
})
|
||||
|
||||
const labelClass = computed(() => {
|
||||
if (props.variant === 'success') return 'text-green-700'
|
||||
if (props.variant === 'warning') return 'text-amber-700'
|
||||
if (props.variant === 'danger') return 'text-red-700'
|
||||
return 'text-gray-500'
|
||||
})
|
||||
|
||||
const valueClass = computed(() => {
|
||||
if (props.valueClass) return props.valueClass
|
||||
if (props.variant === 'success') return 'text-green-700'
|
||||
if (props.variant === 'warning') return 'text-amber-700'
|
||||
if (props.variant === 'danger') return 'text-red-600'
|
||||
return 'text-gray-900'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<BasicActionSheet
|
||||
v-model:visible="visibleModel"
|
||||
:title="title"
|
||||
:showButton="false"
|
||||
:drawerHeight="drawerHeight"
|
||||
>
|
||||
<div v-if="item" class="space-y-3 pb-2">
|
||||
<slot :item="item" />
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { ACTION_SHEET_HEIGHT } from '@/constants/actionSheet'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
item: T | null
|
||||
title?: string
|
||||
drawerHeight?: string
|
||||
}>(),
|
||||
{
|
||||
drawerHeight: ACTION_SHEET_HEIGHT
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}>()
|
||||
|
||||
const visibleModel = computed({
|
||||
get: () => props.visible,
|
||||
set: (value: boolean) => emit('update:visible', value)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="flex shrink-0 items-center justify-center transition-colors"
|
||||
:class="
|
||||
plain
|
||||
? 'h-auto w-auto border-none bg-transparent p-0'
|
||||
: 'h-9 w-9 rounded-full border-none bg-white active:bg-gray-50'
|
||||
"
|
||||
@click.stop="$emit('click')"
|
||||
>
|
||||
<BaseIcon :icon="icon" :size="size" :color="color" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
icon: string
|
||||
color?: string
|
||||
size?: number
|
||||
plain?: boolean
|
||||
}>(),
|
||||
{
|
||||
color: '#374151',
|
||||
size: 18,
|
||||
plain: false
|
||||
}
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs text-gray-500">{{ label }}</p>
|
||||
<p class="truncate text-sm font-medium text-gray-900" :class="valueClass">
|
||||
<slot>{{ value }}</slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
value?: string | number
|
||||
valueClass?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="grid gap-3" :class="gridClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
cols?: 1 | 2
|
||||
}>(),
|
||||
{ cols: 2 }
|
||||
)
|
||||
|
||||
const gridClass = computed(() => (props.cols === 1 ? 'grid-cols-1' : 'grid-cols-2'))
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div v-if="showTitle" class="text-lg font-700 text-gray-900">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</div>
|
||||
<div v-if="subtitle || $slots.subtitle" class="text-xs text-gray-500">
|
||||
<slot name="subtitle">{{ subtitle }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
subtitle?: string
|
||||
showTitle?: boolean
|
||||
}>(),
|
||||
{
|
||||
showTitle: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-4 py-3" :class="containerClass">
|
||||
<div class="flex items-center">
|
||||
<slot name="prefix" />
|
||||
<span class="font-700" :class="[labelClass, $slots.prefix ? 'mr-2' : '']">{{ label }}</span>
|
||||
</div>
|
||||
<div class="font-800" :class="valueClass">
|
||||
<slot>{{ value }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
value?: string | number
|
||||
variant?: 'success' | 'danger' | 'neutral'
|
||||
bordered?: boolean
|
||||
}>(),
|
||||
{
|
||||
variant: 'neutral',
|
||||
bordered: false
|
||||
}
|
||||
)
|
||||
|
||||
const containerClass = computed(() => {
|
||||
const radius = props.bordered ? 'rounded-xl border' : 'rounded-xl'
|
||||
if (props.variant === 'success') return `${radius} border-green-200 bg-green-50`
|
||||
if (props.variant === 'danger') return `${radius} border-red-200 bg-red-50`
|
||||
return `${radius} border-gray-200 bg-gray-50`
|
||||
})
|
||||
|
||||
const labelClass = computed(() => {
|
||||
if (props.variant === 'success') return 'text-green-700'
|
||||
if (props.variant === 'danger') return 'text-red-600'
|
||||
return 'text-gray-500'
|
||||
})
|
||||
|
||||
const valueClass = computed(() => {
|
||||
if (props.variant === 'success') return 'text-lg text-green-700'
|
||||
if (props.variant === 'danger') return 'text-lg text-red-600'
|
||||
return 'text-gray-900'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
export function useMobileDetailSheet<T extends { id?: number }>() {
|
||||
const sheetVisible = ref(false)
|
||||
const selectedItem = ref<T | null>(null)
|
||||
|
||||
function openSheet(item: T) {
|
||||
selectedItem.value = item
|
||||
sheetVisible.value = true
|
||||
}
|
||||
|
||||
function syncSelectedItem(items: T[]) {
|
||||
if (!selectedItem.value?.id) return
|
||||
const updated = items.find((item) => item.id === selectedItem.value?.id)
|
||||
if (updated) selectedItem.value = updated
|
||||
}
|
||||
|
||||
return {
|
||||
sheetVisible,
|
||||
selectedItem,
|
||||
openSheet,
|
||||
syncSelectedItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { TableInput, TableOutput } from '@core/type/baseTable'
|
||||
|
||||
interface Options<T> {
|
||||
fetchApi: (params: TableInput) => Promise<TableOutput<T>>
|
||||
onFetched?: (items: T[]) => void
|
||||
}
|
||||
|
||||
export function useMobileTableList<T extends { id?: number }>(options: Options<T>) {
|
||||
const loading = ref(false)
|
||||
const search = ref('')
|
||||
const query = ref<TableInput>({ page: 1, limit: 10, select: [] })
|
||||
const total = ref(0)
|
||||
const items = ref<T[]>([]) as Ref<T[]>
|
||||
|
||||
const hasMore = computed(() => items.value.length < total.value)
|
||||
|
||||
async function fetchList(append = false) {
|
||||
try {
|
||||
loading.value = true
|
||||
const params: TableInput = {
|
||||
...query.value,
|
||||
search: search.value || null
|
||||
}
|
||||
const res = await options.fetchApi(params)
|
||||
query.value = res.query
|
||||
total.value = res.query.total ?? 0
|
||||
items.value = append ? [...items.value, ...res.rows] : res.rows
|
||||
options.onFetched?.(items.value)
|
||||
} catch {
|
||||
total.value = 0
|
||||
items.value = []
|
||||
options.onFetched?.([])
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
query.value.page = 1
|
||||
await fetchList(false)
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
query.value.page = (query.value.page ?? 1) + 1
|
||||
await fetchList(true)
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
search,
|
||||
items,
|
||||
hasMore,
|
||||
reload,
|
||||
loadMore,
|
||||
fetch: reload
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Wallet } from '@/model/wallet'
|
||||
|
||||
export function getWallets(wallet?: Wallet | Wallet[]): Wallet[] {
|
||||
if (!wallet) return []
|
||||
return Array.isArray(wallet) ? wallet : [wallet]
|
||||
}
|
||||
|
||||
export function getCashBalance(wallet?: Wallet | Wallet[]): number {
|
||||
return getWallets(wallet)[0]?.cash ?? 0
|
||||
}
|
||||
|
||||
export function getGoldBalance(wallet?: Wallet | Wallet[]): string {
|
||||
const sum = getWallets(wallet).reduce((total, item) => total + (item.gold ?? 0), 0)
|
||||
return sum.toFixed(3)
|
||||
}
|
||||
Reference in New Issue
Block a user