Initial commit

This commit is contained in:
2026-04-23 16:58:11 +08:00
commit 267eba1eca
2582 changed files with 273338 additions and 0 deletions

View File

@@ -0,0 +1,341 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { Search } from '@/components/Search'
import { Dialog } from '@/components/Dialog'
import { useI18n } from '@/hooks/web/useI18n'
import { ElTag } from 'element-plus'
import { Table } from '@/components/Table'
import {
getDepartmentApi,
getDepartmentTableApi,
saveDepartmentApi,
deleteDepartmentApi
} from '@/api/department'
import type { DepartmentItem } from '@/api/department/types'
import { useTable } from '@/hooks/web/useTable'
import { ref, unref, reactive } from 'vue'
import Write from './components/Write.vue'
import Detail from './components/Detail.vue'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { BaseButton } from '@/components/Button'
const ids = ref<string[]>([])
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const { currentPage, pageSize } = tableState
const res = await getDepartmentTableApi({
pageIndex: unref(currentPage),
pageSize: unref(pageSize),
...unref(searchParams)
})
return {
list: res.data.list,
total: res.data.total
}
},
fetchDelApi: async () => {
const res = await deleteDepartmentApi(unref(ids))
return !!res
}
})
const { loading, dataList, total, currentPage, pageSize } = tableState
const { getList, getElTableExpose, delList } = tableMethods
const searchParams = ref({})
const setSearchParams = (params: any) => {
searchParams.value = params
getList()
}
const { t } = useI18n()
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
},
{
field: 'id',
label: t('userDemo.departmentName'),
table: {
slots: {
default: (data: any) => {
return <>{data.row.departmentName}</>
}
}
},
form: {
component: 'TreeSelect',
componentProps: {
nodeKey: 'id',
props: {
label: 'departmentName'
}
},
optionApi: async () => {
const res = await getDepartmentApi()
return res.data.list
}
},
detail: {
slots: {
default: (data: any) => {
return <>{data.departmentName}</>
}
}
}
},
{
field: 'status',
label: t('userDemo.status'),
search: {
hidden: true
},
table: {
slots: {
default: (data: any) => {
const status = data.row.status
return (
<>
<ElTag type={status === 0 ? 'danger' : 'success'}>
{status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
</ElTag>
</>
)
}
}
},
form: {
component: 'Select',
componentProps: {
options: [
{
value: 0,
label: t('userDemo.disable')
},
{
value: 1,
label: t('userDemo.enable')
}
]
}
},
detail: {
slots: {
default: (data: any) => {
return (
<>
<ElTag type={data.status === 0 ? 'danger' : 'success'}>
{data.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
</ElTag>
</>
)
}
}
}
},
{
field: 'createTime',
label: t('tableDemo.displayTime'),
search: {
hidden: true
},
form: {
hidden: true
}
},
{
field: 'remark',
label: t('userDemo.remark'),
search: {
hidden: true
},
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 5
},
colProps: {
span: 24
}
},
detail: {
slots: {
default: (data: any) => {
return <>{data.remark}</>
}
}
}
},
{
field: 'action',
width: '260px',
label: t('tableDemo.action'),
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
slots: {
default: (data: any) => {
return (
<>
<BaseButton type="primary" onClick={() => action(data.row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(data.row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger" onClick={() => delData(data.row)}>
{t('exampleDemo.del')}
</BaseButton>
</>
)
}
}
}
}
])
// @ts-ignore
const { allSchemas } = useCrudSchemas(crudSchemas)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentRow = ref<DepartmentItem | null>(null)
const actionType = ref('')
const AddAction = () => {
dialogTitle.value = t('exampleDemo.add')
currentRow.value = null
dialogVisible.value = true
actionType.value = ''
}
const delLoading = ref(false)
const delData = async (row: DepartmentItem | null) => {
const elTableExpose = await getElTableExpose()
ids.value = row
? [row.id]
: elTableExpose?.getSelectionRows().map((v: DepartmentItem) => v.id) || []
delLoading.value = true
await delList(unref(ids).length).finally(() => {
delLoading.value = false
})
}
const action = (row: DepartmentItem, type: string) => {
dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
actionType.value = type
currentRow.value = row
dialogVisible.value = true
}
const writeRef = ref<ComponentRef<typeof Write>>()
const saveLoading = ref(false)
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
saveLoading.value = true
const res = await saveDepartmentApi(formData)
.catch(() => {})
.finally(() => {
saveLoading.value = false
})
if (res) {
dialogVisible.value = false
currentPage.value = 1
getList()
}
}
}
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
<BaseButton :loading="delLoading" type="danger" @click="delData(null)">
{{ t('exampleDemo.del') }}
</BaseButton>
</div>
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"
:columns="allSchemas.tableColumns"
:data="dataList"
:loading="loading"
:pagination="{
total: total
}"
@register="tableRegister"
/>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Write
v-if="actionType !== 'detail'"
ref="writeRef"
:form-schema="allSchemas.formSchema"
:current-row="currentRow"
/>
<Detail
v-if="actionType === 'detail'"
:detail-schema="allSchemas.detailSchema"
:current-row="currentRow"
/>
<template #footer>
<BaseButton
v-if="actionType !== 'detail'"
type="primary"
:loading="saveLoading"
@click="save"
>
{{ t('exampleDemo.save') }}
</BaseButton>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { PropType } from 'vue'
import { DepartmentItem } from '@/api/department/types'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
defineProps({
currentRow: {
type: Object as PropType<Nullable<DepartmentItem>>,
default: () => null
},
detailSchema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
}
})
</script>
<template>
<Descriptions :schema="detailSchema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { DepartmentItem } from '@/api/department/types'
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<DepartmentItem>>,
default: () => null
},
formSchema: {
type: Array as PropType<FormSchema[]>,
default: () => []
}
})
const rules = reactive({
id: [required()],
status: [required()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose } = formMethods
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
return formData
}
}
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
</template>

View File

@@ -0,0 +1,212 @@
<script setup lang="tsx">
import { reactive, ref, unref } from 'vue'
import { getMenuListApi } from '@/api/menu'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { ElTag } from 'element-plus'
import { Icon } from '@/components/Icon'
import { Search } from '@/components/Search'
import { FormSchema } from '@/components/Form'
import { ContentWrap } from '@/components/ContentWrap'
import Write from './components/Write.vue'
import Detail from './components/Detail.vue'
import { Dialog } from '@/components/Dialog'
import { BaseButton } from '@/components/Button'
const { t } = useI18n()
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const res = await getMenuListApi()
return {
list: res.data.list || []
}
}
})
const { dataList, loading } = tableState
const { getList } = tableMethods
const tableColumns = reactive<TableColumn[]>([
{
field: 'index',
label: t('userDemo.index'),
type: 'index'
},
{
field: 'meta.title',
label: t('menu.menuName'),
slots: {
default: (data: any) => {
const title = data.row.meta.title
return <>{title}</>
}
}
},
{
field: 'meta.icon',
label: t('menu.icon'),
slots: {
default: (data: any) => {
const icon = data.row.meta.icon
if (icon) {
return (
<>
<Icon icon={icon} />
</>
)
} else {
return null
}
}
}
},
// {
// field: 'meta.permission',
// label: t('menu.permission'),
// slots: {
// default: (data: any) => {
// const permission = data.row.meta.permission
// return permission ? <>{permission.join(', ')}</> : null
// }
// }
// },
{
field: 'component',
label: t('menu.component'),
slots: {
default: (data: any) => {
const component = data.row.component
return <>{component === '#' ? '顶级目录' : component === '##' ? '子目录' : component}</>
}
}
},
{
field: 'path',
label: t('menu.path')
},
{
field: 'status',
label: t('menu.status'),
slots: {
default: (data: any) => {
return (
<>
<ElTag type={data.row.status === 0 ? 'danger' : 'success'}>
{data.row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
</ElTag>
</>
)
}
}
},
{
field: 'action',
label: t('userDemo.action'),
width: 240,
slots: {
default: (data: any) => {
const row = data.row
return (
<>
<BaseButton type="primary" onClick={() => action(row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger">{t('exampleDemo.del')}</BaseButton>
</>
)
}
}
}
])
const searchSchema = reactive<FormSchema[]>([
{
field: 'meta.title',
label: t('menu.menuName'),
component: 'Input'
}
])
const searchParams = ref({})
const setSearchParams = (data: any) => {
searchParams.value = data
getList()
}
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentRow = ref()
const actionType = ref('')
const writeRef = ref<ComponentRef<typeof Write>>()
const saveLoading = ref(false)
const action = (row: any, type: string) => {
dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
actionType.value = type
currentRow.value = row
dialogVisible.value = true
}
const AddAction = () => {
dialogTitle.value = t('exampleDemo.add')
currentRow.value = undefined
dialogVisible.value = true
actionType.value = ''
}
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
console.log(formData)
if (formData) {
saveLoading.value = true
setTimeout(() => {
saveLoading.value = false
dialogVisible.value = false
}, 1000)
}
}
</script>
<template>
<ContentWrap>
<Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
</div>
<Table
:columns="tableColumns"
default-expand-all
node-key="id"
:data="dataList"
:loading="loading"
@register="tableRegister"
/>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Write v-if="actionType !== 'detail'" ref="writeRef" :current-row="currentRow" />
<Detail v-if="actionType === 'detail'" :current-row="currentRow" />
<template #footer>
<BaseButton
v-if="actionType !== 'detail'"
type="primary"
:loading="saveLoading"
@click="save"
>
{{ t('exampleDemo.save') }}
</BaseButton>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import { FormSchema, Form } from '@/components/Form'
import { ElDrawer } from 'element-plus'
import { reactive } from 'vue'
import { useForm } from '@/hooks/web/useForm'
import { useValidator } from '@/hooks/web/useValidator'
const modelValue = defineModel<boolean>()
const { required } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'label',
label: 'label',
component: 'Input',
colProps: {
span: 24
}
},
{
field: 'value',
label: 'value',
component: 'Input',
colProps: {
span: 24
}
}
])
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose } = formMethods
const emit = defineEmits(['confirm'])
const rules = reactive({
label: [required()],
value: [required()]
})
const confirm = async () => {
const elFormExpose = await getElFormExpose()
if (!elFormExpose) return
const valid = await elFormExpose?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
formData.id = Date.now()
emit('confirm', formData)
modelValue.value = false
}
}
</script>
<template>
<ElDrawer v-model="modelValue" title="新增按钮权限">
<template #default>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
</template>
<template #footer>
<div>
<BaseButton @click="() => (modelValue = false)">取消</BaseButton>
<BaseButton type="primary" @click="confirm">确认</BaseButton>
</div>
</template>
</ElDrawer>
</template>

View File

@@ -0,0 +1,173 @@
<script setup lang="tsx">
import { PropType, ref } from 'vue'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
import { Icon } from '@/components/Icon'
import { ElTag } from 'element-plus'
defineProps({
currentRow: {
type: Object as PropType<any>,
default: () => undefined
}
})
const renderTag = (enable?: boolean) => {
return <ElTag type={!enable ? 'danger' : 'success'}>{enable ? '启用' : '禁用'}</ElTag>
}
const detailSchema = ref<DescriptionsSchema[]>([
{
field: 'type',
label: '菜单类型',
span: 24,
slots: {
default: (data) => {
const type = data.type
return <>{type === 1 ? '菜单' : '目录'}</>
}
}
},
{
field: 'parentName',
label: '父级菜单'
},
{
field: 'meta.title',
label: '菜单名称'
},
{
field: 'component',
label: '组件',
slots: {
default: (data) => {
const component = data.component
return <>{component === '#' ? '顶级目录' : component === '##' ? '子目录' : component}</>
}
}
},
{
field: 'name',
label: '组件名称'
},
{
field: 'meta.icon',
label: '图标',
slots: {
default: (data) => {
const icon = data.icon
if (icon) {
return (
<>
<Icon icon={icon} />
</>
)
} else {
return null
}
}
}
},
{
field: 'path',
label: '路径'
},
{
field: 'meta.activeMenu',
label: '高亮菜单'
},
{
field: 'permissionList',
label: '按钮权限',
span: 24,
slots: {
default: (data: any) => (
<>
{data?.permissionList?.map((v) => {
return (
<ElTag class="mr-1" key={v.value}>
{v.label}
</ElTag>
)
})}
</>
)
}
},
{
field: 'menuState',
label: '菜单状态',
slots: {
default: (data) => {
return renderTag(data.menuState)
}
}
},
{
field: 'meta.hidden',
label: '是否隐藏',
slots: {
default: (data) => {
return renderTag(data.enableHidden)
}
}
},
{
field: 'meta.alwaysShow',
label: '是否一直显示',
slots: {
default: (data) => {
return renderTag(data.enableDisplay)
}
}
},
{
field: 'meta.noCache',
label: '是否清除缓存',
slots: {
default: (data) => {
return renderTag(data.enableCleanCache)
}
}
},
{
field: 'meta.breadcrumb',
label: '是否显示面包屑',
slots: {
default: (data) => {
return renderTag(data.enableShowCrumb)
}
}
},
{
field: 'meta.affix',
label: '是否固定标签页',
slots: {
default: (data) => {
return renderTag(data.enablePinnedTab)
}
}
},
{
field: 'meta.noTagsView',
label: '是否隐藏标签页',
slots: {
default: (data) => {
return renderTag(data.enableHiddenTab)
}
}
},
{
field: 'meta.canTo',
label: '是否可跳转',
slots: {
default: (data) => {
return renderTag(data.enableSkip)
}
}
}
])
</script>
<template>
<Descriptions :schema="detailSchema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,409 @@
<script setup lang="tsx">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch, ref, unref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { useI18n } from '@/hooks/web/useI18n'
import { getMenuListApi } from '@/api/menu'
import { ElButton, ElInput, ElPopconfirm, ElTable, ElTableColumn, ElTag } from 'element-plus'
import AddButtonPermission from './AddButtonPermission.vue'
import { BaseButton } from '@/components/Button'
import { cloneDeep } from 'lodash-es'
const { t } = useI18n()
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<any>,
default: () => null
}
})
const handleClose = async (tag: any) => {
const formData = await getFormData()
// 删除对应的权限
setValues({
permissionList: formData?.permissionList?.filter((v: any) => v.value !== tag.value)
})
}
const handleEdit = async (row: any) => {
// 深拷贝当前行数据到编辑行
permissionEditingRow.value = { ...row }
}
const handleSave = async () => {
const formData = await getFormData()
const index = formData?.permissionList?.findIndex((x) => x.id === permissionEditingRow.value.id)
if (index !== -1) {
formData.permissionList[index] = { ...permissionEditingRow.value }
permissionEditingRow.value = null // 重置编辑状态
}
}
const showDrawer = ref(false)
// 存储正在编辑的行的数据
const permissionEditingRow = ref<any>(null)
const formSchema = reactive<FormSchema[]>([
{
field: 'type',
label: '菜单类型',
component: 'RadioButton',
value: 0,
colProps: {
span: 24
},
componentProps: {
options: [
{
label: '目录',
value: 0
},
{
label: '菜单',
value: 1
}
],
on: {
change: async (val: number) => {
const formData = await getFormData()
if (val === 1) {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: false
}
])
setValues({
component: unref(cacheComponent)
})
} else {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: true
}
])
if (formData.parentId === void 0) {
setValues({
component: '#'
})
} else {
setValues({
component: '##'
})
}
}
}
}
}
},
{
field: 'parentId',
label: '父级菜单',
component: 'TreeSelect',
componentProps: {
nodeKey: 'id',
props: {
label: 'title',
value: 'id',
children: 'children'
},
highlightCurrent: true,
expandOnClickNode: false,
checkStrictly: true,
checkOnClickNode: true,
clearable: true,
on: {
change: async (val: number) => {
const formData = await getFormData()
if (val && formData.type === 0) {
setValues({
component: '##'
})
} else if (!val && formData.type === 0) {
setValues({
component: '#'
})
} else if (formData.type === 1) {
setValues({
component: unref(cacheComponent) ?? ''
})
}
}
}
},
optionApi: async () => {
const res = await getMenuListApi()
return res.data.list || []
}
},
{
field: 'meta.title',
label: t('menu.menuName'),
component: 'Input'
},
{
field: 'component',
label: '组件',
component: 'Input',
value: '#',
componentProps: {
disabled: true,
placeholder: '#为顶级目录,##为子目录',
on: {
change: (val: string) => {
cacheComponent.value = val
}
}
}
},
{
field: 'name',
label: t('menu.name'),
component: 'Input'
},
{
field: 'meta.icon',
label: t('menu.icon'),
component: 'Input'
},
{
field: 'path',
label: t('menu.path'),
component: 'Input'
},
{
field: 'meta.activeMenu',
label: t('menu.activeMenu'),
component: 'Input'
},
{
field: 'status',
label: t('menu.status'),
component: 'Select',
componentProps: {
options: [
{
label: t('userDemo.disable'),
value: 0
},
{
label: t('userDemo.enable'),
value: 1
}
]
}
},
{
field: 'permissionList',
label: t('menu.permission'),
component: 'CheckboxGroup',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: (data: any) => (
<>
<BaseButton
class="m-t-5px"
type="primary"
size="small"
onClick={() => (showDrawer.value = true)}
>
添加权限
</BaseButton>
<ElTable data={data?.permissionList}>
<ElTableColumn type="index" prop="id" />
<ElTableColumn
prop="value"
label="Value"
v-slots={{
default: ({ row }: any) =>
permissionEditingRow.value && permissionEditingRow.value.id === row.id ? (
<ElInput v-model={permissionEditingRow.value.value} size="small" />
) : (
<span>{row.value}</span>
)
}}
/>
<ElTableColumn
prop="label"
label="Label"
v-slots={{
default: ({ row }: any) =>
permissionEditingRow.value && permissionEditingRow.value.id === row.id ? (
<ElInput v-model={permissionEditingRow.value.label} size="small" />
) : (
<ElTag class="mr-1" key={row.value}>
{row.label}
</ElTag>
)
}}
/>
<ElTableColumn
label="Operations"
width="180"
v-slots={{
default: ({ row }: any) =>
permissionEditingRow.value && permissionEditingRow.value.id === row.id ? (
<ElButton size="small" type="primary" onClick={handleSave}>
确定
</ElButton>
) : (
<>
<ElButton size="small" type="primary" onClick={() => handleEdit(row)}>
编辑
</ElButton>
<ElPopconfirm
title="Are you sure to delete this?"
onConfirm={() => handleClose(row)}
>
{{
reference: () => (
<ElButton size="small" type="danger">
删除
</ElButton>
)
}}
</ElPopconfirm>
</>
)
}}
/>
</ElTable>
</>
)
}
}
},
{
field: 'meta.hidden',
label: t('menu.hidden'),
component: 'Switch'
},
{
field: 'meta.alwaysShow',
label: t('menu.alwaysShow'),
component: 'Switch'
},
{
field: 'meta.noCache',
label: t('menu.noCache'),
component: 'Switch'
},
{
field: 'meta.breadcrumb',
label: t('menu.breadcrumb'),
component: 'Switch'
},
{
field: 'meta.affix',
label: t('menu.affix'),
component: 'Switch'
},
{
field: 'meta.noTagsView',
label: t('menu.noTagsView'),
component: 'Switch'
},
{
field: 'meta.canTo',
label: t('menu.canTo'),
component: 'Switch'
}
])
const rules = reactive({
component: [required()],
path: [required()],
'meta.title': [required()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
return formData
}
}
const cacheComponent = ref('')
watch(
() => props.currentRow,
(value) => {
if (!value) return
const currentRow = cloneDeep(value)
cacheComponent.value = currentRow.type === 1 ? currentRow.component : ''
if (currentRow.parentId === 0) {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: true
}
])
} else {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: false
}
])
}
if (currentRow.type === 1) {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: false
}
])
} else {
setSchema([
{
field: 'component',
path: 'componentProps.disabled',
value: true
}
])
}
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
const confirm = async (data: any) => {
const formData = await getFormData()
setValues({
permissionList: [...(formData?.permissionList || []), data]
})
}
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
<AddButtonPermission v-model="showDrawer" @confirm="confirm" />
</template>

View File

@@ -0,0 +1,173 @@
<script setup lang="tsx">
import { reactive, ref, unref } from 'vue'
import { getRoleListApi } from '@/api/role'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { ElTag } from 'element-plus'
import { Search } from '@/components/Search'
import { FormSchema } from '@/components/Form'
import { ContentWrap } from '@/components/ContentWrap'
import Write from './components/Write.vue'
import Detail from './components/Detail.vue'
import { Dialog } from '@/components/Dialog'
import { BaseButton } from '@/components/Button'
const { t } = useI18n()
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const res = await getRoleListApi()
return {
list: res.data.list || [],
total: res.data.total
}
}
})
const { dataList, loading, total } = tableState
const { getList } = tableMethods
const tableColumns = reactive<TableColumn[]>([
{
field: 'index',
label: t('userDemo.index'),
type: 'index'
},
{
field: 'roleName',
label: t('role.roleName')
},
{
field: 'status',
label: t('menu.status'),
slots: {
default: (data: any) => {
return (
<>
<ElTag type={data.row.status === 0 ? 'danger' : 'success'}>
{data.row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
</ElTag>
</>
)
}
}
},
{
field: 'createTime',
label: t('tableDemo.displayTime')
},
{
field: 'remark',
label: t('userDemo.remark')
},
{
field: 'action',
label: t('userDemo.action'),
width: 240,
slots: {
default: (data: any) => {
const row = data.row
return (
<>
<BaseButton type="primary" onClick={() => action(row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger">{t('exampleDemo.del')}</BaseButton>
</>
)
}
}
}
])
const searchSchema = reactive<FormSchema[]>([
{
field: 'roleName',
label: t('role.roleName'),
component: 'Input'
}
])
const searchParams = ref({})
const setSearchParams = (data: any) => {
searchParams.value = data
getList()
}
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentRow = ref()
const actionType = ref('')
const writeRef = ref<ComponentRef<typeof Write>>()
const saveLoading = ref(false)
const action = (row: any, type: string) => {
dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
actionType.value = type
currentRow.value = row
dialogVisible.value = true
}
const AddAction = () => {
dialogTitle.value = t('exampleDemo.add')
currentRow.value = undefined
dialogVisible.value = true
actionType.value = ''
}
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
saveLoading.value = true
setTimeout(() => {
saveLoading.value = false
dialogVisible.value = false
}, 1000)
}
}
</script>
<template>
<ContentWrap>
<Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
</div>
<Table
:columns="tableColumns"
default-expand-all
node-key="id"
:data="dataList"
:loading="loading"
:pagination="{
total
}"
@register="tableRegister"
/>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Write v-if="actionType !== 'detail'" ref="writeRef" :current-row="currentRow" />
<Detail v-else :current-row="currentRow" />
<template #footer>
<BaseButton
v-if="actionType !== 'detail'"
type="primary"
:loading="saveLoading"
@click="save"
>
{{ t('exampleDemo.save') }}
</BaseButton>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,106 @@
<script setup lang="tsx">
import { PropType, ref, unref, nextTick } from 'vue'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
import { ElTag, ElTree } from 'element-plus'
import { findIndex } from '@/utils'
import { getMenuListApi } from '@/api/menu'
defineProps({
currentRow: {
type: Object as PropType<any>,
default: () => undefined
}
})
const filterPermissionName = (value: string) => {
const index = findIndex(unref(currentTreeData)?.permissionList || [], (item) => {
return item.value === value
})
return (unref(currentTreeData)?.permissionList || [])[index].label ?? ''
}
const renderTag = (enable?: boolean) => {
return <ElTag type={!enable ? 'danger' : 'success'}>{enable ? '启用' : '禁用'}</ElTag>
}
const treeRef = ref<typeof ElTree>()
const currentTreeData = ref()
const nodeClick = (treeData: any) => {
currentTreeData.value = treeData
}
const treeData = ref<any[]>([])
const getMenuList = async () => {
const res = await getMenuListApi()
if (res) {
treeData.value = res.data.list
await nextTick()
}
}
getMenuList()
const detailSchema = ref<DescriptionsSchema[]>([
{
field: 'roleName',
label: '角色名称'
},
{
field: 'status',
label: '状态',
slots: {
default: (data: any) => {
return renderTag(data.status)
}
}
},
{
field: 'remark',
label: '备注',
span: 24
},
{
field: 'permissionList',
label: '菜单分配',
span: 24,
slots: {
default: () => {
return (
<>
<div class="flex w-full">
<div class="flex-1">
<ElTree
ref={treeRef}
node-key="id"
props={{ children: 'children', label: 'title' }}
highlight-current
expand-on-click-node={false}
data={treeData.value}
onNode-click={nodeClick}
>
{{
default: (data) => {
return <span>{data?.data?.title}</span>
}
}}
</ElTree>
</div>
<div class="flex-1">
{unref(currentTreeData)
? unref(currentTreeData)?.meta?.permission?.map((v: string) => {
return <ElTag class="ml-2 mt-2">{filterPermissionName(v)}</ElTag>
})
: null}
</div>
</div>
</>
)
}
}
}
])
</script>
<template>
<Descriptions :schema="detailSchema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,181 @@
<script setup lang="tsx">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch, ref, unref, nextTick } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { useI18n } from '@/hooks/web/useI18n'
import { ElTree, ElCheckboxGroup, ElCheckbox } from 'element-plus'
import { getMenuListApi } from '@/api/menu'
import { filter, eachTree } from '@/utils/tree'
import { findIndex } from '@/utils'
const { t } = useI18n()
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<any>,
default: () => null
}
})
const treeRef = ref<typeof ElTree>()
const formSchema = ref<FormSchema[]>([
{
field: 'roleName',
label: t('role.roleName'),
component: 'Input'
},
{
field: 'status',
label: t('menu.status'),
component: 'Select',
componentProps: {
options: [
{
label: t('userDemo.disable'),
value: 0
},
{
label: t('userDemo.enable'),
value: 1
}
]
}
},
{
field: 'menu',
label: t('role.menu'),
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="flex w-full">
<div class="flex-1">
<ElTree
ref={treeRef}
show-checkbox
node-key="id"
highlight-current
check-strictly
expand-on-click-node={false}
data={treeData.value}
onNode-click={nodeClick}
>
{{
default: (data) => {
return <span>{data.data.meta.title}</span>
}
}}
</ElTree>
</div>
<div class="flex-1">
{unref(currentTreeData) && unref(currentTreeData)?.permissionList ? (
<ElCheckboxGroup v-model={unref(currentTreeData).meta.permission}>
{unref(currentTreeData)?.permissionList.map((v: any) => {
return <ElCheckbox label={v.value}>{v.label}</ElCheckbox>
})}
</ElCheckboxGroup>
) : null}
</div>
</div>
</>
)
}
}
}
}
])
const currentTreeData = ref()
const nodeClick = (treeData: any) => {
currentTreeData.value = treeData
}
const rules = reactive({
roleName: [required()],
role: [required()],
status: [required()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose } = formMethods
const treeData = ref([])
const getMenuList = async () => {
const res = await getMenuListApi()
if (res) {
treeData.value = res.data.list
if (!props.currentRow) return
await nextTick()
const checked: any[] = []
eachTree(props.currentRow.menu, (v) => {
checked.push({
id: v.id,
permission: v.meta?.permission || []
})
})
eachTree(treeData.value, (v) => {
const index = findIndex(checked, (item) => {
return item.id === v.id
})
if (index > -1) {
const meta = { ...(v.meta || {}) }
meta.permission = checked[index].permission
v.meta = meta
}
})
for (const item of checked) {
unref(treeRef)?.setChecked(item.id, true, false)
}
// unref(treeRef)?.setCheckedKeys(
// checked.map((v) => v.id),
// false
// )
}
}
getMenuList()
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
const checkedKeys = unref(treeRef)?.getCheckedKeys() || []
const data = filter(unref(treeData), (item: any) => {
return checkedKeys.includes(item.id)
})
formData.menu = data || []
console.log(formData)
return formData
}
}
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
</template>

View File

@@ -0,0 +1,384 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table } from '@/components/Table'
import { ref, unref, nextTick, watch, reactive } from 'vue'
import { ElTree, ElInput, ElDivider } from 'element-plus'
import { getDepartmentApi, getUserByIdApi, saveUserApi, deleteUserByIdApi } from '@/api/department'
import type { DepartmentItem, DepartmentUserItem } from '@/api/department/types'
import { useTable } from '@/hooks/web/useTable'
import { Search } from '@/components/Search'
import Write from './components/Write.vue'
import Detail from './components/Detail.vue'
import { Dialog } from '@/components/Dialog'
import { getRoleListApi } from '@/api/role'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { BaseButton } from '@/components/Button'
const { t } = useI18n()
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const { pageSize, currentPage } = tableState
const res = await getUserByIdApi({
id: unref(currentNodeKey),
pageIndex: unref(currentPage),
pageSize: unref(pageSize),
...unref(searchParams)
})
return {
list: res.data.list || [],
total: res.data.total || 0
}
},
fetchDelApi: async () => {
const res = await deleteUserByIdApi(unref(ids))
return !!res
}
})
const { total, loading, dataList, pageSize, currentPage } = tableState
const { getList, getElTableExpose, delList } = tableMethods
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('userDemo.index'),
form: {
hidden: true
},
search: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'index'
}
},
{
field: 'username',
label: t('userDemo.username')
},
{
field: 'account',
label: t('userDemo.account')
},
{
field: 'department.id',
label: t('userDemo.department'),
detail: {
hidden: true
// slots: {
// default: (data: DepartmentUserItem) => {
// return <>{data.department.departmentName}</>
// }
// }
},
search: {
hidden: true
},
form: {
component: 'TreeSelect',
componentProps: {
nodeKey: 'id',
props: {
label: 'departmentName'
}
},
optionApi: async () => {
const res = await getDepartmentApi()
return res.data.list
}
},
table: {
hidden: true
}
},
{
field: 'role',
label: t('userDemo.role'),
search: {
hidden: true
},
form: {
component: 'Select',
value: [],
componentProps: {
multiple: true,
collapseTags: true,
maxCollapseTags: 1
},
optionApi: async () => {
const res = await getRoleListApi()
return res.data?.list?.map((v) => ({
label: v.roleName,
value: v.id
}))
}
}
},
{
field: 'email',
label: t('userDemo.email'),
form: {
component: 'Input'
},
search: {
hidden: true
}
},
{
field: 'createTime',
label: t('userDemo.createTime'),
form: {
component: 'Input'
},
search: {
hidden: true
}
},
{
field: 'action',
label: t('userDemo.action'),
form: {
hidden: true
},
detail: {
hidden: true
},
search: {
hidden: true
},
table: {
width: 240,
slots: {
default: (data: any) => {
const row = data.row as DepartmentUserItem
return (
<>
<BaseButton type="primary" onClick={() => action(row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger" onClick={() => delData(row)}>
{t('exampleDemo.del')}
</BaseButton>
</>
)
}
}
}
}
])
const { allSchemas } = useCrudSchemas(crudSchemas)
const searchParams = ref({})
const setSearchParams = (params: any) => {
currentPage.value = 1
searchParams.value = params
getList()
}
const treeEl = ref<typeof ElTree>()
const currentNodeKey = ref('')
const departmentList = ref<DepartmentItem[]>([])
const fetchDepartment = async () => {
const res = await getDepartmentApi()
departmentList.value = res.data.list
currentNodeKey.value =
(res.data.list[0] && res.data.list[0]?.children && res.data.list[0].children[0].id) || ''
await nextTick()
unref(treeEl)?.setCurrentKey(currentNodeKey.value)
}
fetchDepartment()
const currentDepartment = ref('')
watch(
() => currentDepartment.value,
(val) => {
unref(treeEl)!.filter(val)
}
)
const currentChange = (data: DepartmentItem) => {
// if (data.children) return
currentNodeKey.value = data.id
currentPage.value = 1
getList()
}
const filterNode = (value: string, data: DepartmentItem) => {
if (!value) return true
return data.departmentName.includes(value)
}
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentRow = ref<DepartmentUserItem>()
const actionType = ref('')
const AddAction = () => {
dialogTitle.value = t('exampleDemo.add')
currentRow.value = undefined
dialogVisible.value = true
actionType.value = ''
}
const delLoading = ref(false)
const ids = ref<string[]>([])
const delData = async (row?: DepartmentUserItem) => {
const elTableExpose = await getElTableExpose()
ids.value = row
? [row.id]
: elTableExpose?.getSelectionRows().map((v: DepartmentUserItem) => v.id) || []
delLoading.value = true
await delList(unref(ids).length).finally(() => {
delLoading.value = false
})
}
const action = (row: DepartmentUserItem, type: string) => {
dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
actionType.value = type
currentRow.value = { ...row, department: unref(treeEl)?.getCurrentNode() || {} }
dialogVisible.value = true
}
const writeRef = ref<ComponentRef<typeof Write>>()
const saveLoading = ref(false)
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
saveLoading.value = true
try {
const res = await saveUserApi(formData)
if (res) {
currentPage.value = 1
getList()
}
} catch (error) {
console.log(error)
} finally {
saveLoading.value = false
dialogVisible.value = false
}
}
}
</script>
<template>
<div class="flex w-100% h-100%">
<ContentWrap class="w-250px">
<div class="flex justify-center items-center">
<div class="flex-1">{{ t('userDemo.departmentList') }}</div>
<ElInput
v-model="currentDepartment"
class="flex-[2]"
:placeholder="t('userDemo.searchDepartment')"
clearable
/>
</div>
<ElDivider />
<ElTree
ref="treeEl"
:data="departmentList"
default-expand-all
:expand-on-click-node="false"
node-key="id"
:current-node-key="currentNodeKey"
:props="{
label: 'departmentName'
}"
:filter-node-method="filterNode"
@current-change="currentChange"
>
<template #default="{ data }">
<div
:title="data.departmentName"
class="whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{{ data.departmentName }}
</div>
</template>
</ElTree>
</ContentWrap>
<ContentWrap class="flex-[3] ml-20px">
<Search
:schema="allSchemas.searchSchema"
@reset="setSearchParams"
@search="setSearchParams"
/>
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
<BaseButton :loading="delLoading" type="danger" @click="delData()">
{{ t('exampleDemo.del') }}
</BaseButton>
</div>
<Table
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:columns="allSchemas.tableColumns"
:data="dataList"
:loading="loading"
@register="tableRegister"
:pagination="{
total
}"
/>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Write
v-if="actionType !== 'detail'"
ref="writeRef"
:form-schema="allSchemas.formSchema"
:current-row="currentRow"
/>
<Detail
v-if="actionType === 'detail'"
:detail-schema="allSchemas.detailSchema"
:current-row="currentRow"
/>
<template #footer>
<BaseButton
v-if="actionType !== 'detail'"
type="primary"
:loading="saveLoading"
@click="save"
>
{{ t('exampleDemo.save') }}
</BaseButton>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { PropType } from 'vue'
import { DepartmentUserItem } from '@/api/department/types'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
defineProps({
currentRow: {
type: Object as PropType<DepartmentUserItem>,
default: () => undefined
},
detailSchema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
}
})
</script>
<template>
<Descriptions :schema="detailSchema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch } from 'vue'
import { DepartmentUserItem } from '@/api/department/types'
import { useValidator } from '@/hooks/web/useValidator'
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<DepartmentUserItem>,
default: () => undefined
},
formSchema: {
type: Array as PropType<FormSchema[]>,
default: () => []
}
})
const rules = reactive({
username: [required()],
account: [required()],
'department.id': [required()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose } = formMethods
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
return formData
}
}
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Avatars, AvatarItem } from '@/components/Avatars'
import { ref } from 'vue'
const { t } = useI18n()
const data = ref<AvatarItem[]>([
{
name: 'Lily',
url: 'https://avatars.githubusercontent.com/u/3459374?v=4'
},
{
name: 'Amanda',
url: 'https://avatars.githubusercontent.com/u/3459375?v=4'
},
{
name: 'Daisy',
url: 'https://avatars.githubusercontent.com/u/3459376?v=4'
},
{
name: 'Olivia',
url: 'https://avatars.githubusercontent.com/u/3459377?v=4'
},
{
name: 'Tina',
url: 'https://avatars.githubusercontent.com/u/3459378?v=4'
},
{
name: 'Kitty',
url: 'https://avatars.githubusercontent.com/u/3459323?v=4'
},
{
name: 'Helen',
url: 'https://avatars.githubusercontent.com/u/3459324?v=4'
},
{
name: 'Sophia',
url: 'https://avatars.githubusercontent.com/u/3459325?v=4'
},
{
name: 'Wendy',
url: 'https://avatars.githubusercontent.com/u/3459326?v=4'
}
])
</script>
<template>
<ContentWrap :title="t('router.avatars')" :message="t('avatarsDemo.title')">
<Avatars :data="data" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { CountTo } from '@/components/CountTo'
import { ElRow, ElCol, ElInputNumber, ElInput } from 'element-plus'
import { ref, unref } from 'vue'
const { t } = useI18n()
const countRef = ref<ComponentRef<typeof CountTo>>()
const startVal = ref(0)
const endVal = ref(1314512)
const duration = ref(3000)
const decimals = ref(0)
const separator = ref(',')
const prefix = ref('¥ ')
const suffix = ref(' rmb')
const autoplay = ref(false)
const start = () => {
unref(countRef)?.start()
}
const pauseResume = () => {
unref(countRef)?.pauseResume()
}
</script>
<template>
<ContentWrap :title="t('countToDemo.countTo')" :message="t('countToDemo.countToDes')">
<div class="text-center mb-40px">
<CountTo
ref="countRef"
:start-val="startVal"
:end-val="endVal"
:duration="duration"
:decimals="decimals"
:separator="separator"
:prefix="prefix"
:suffix="suffix"
:autoplay="autoplay"
class="text-30px font-bold text-[var(--el-color-primary)]"
/>
</div>
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.startVal') }}</span>
<ElInputNumber v-model="startVal" :min="0" />
</div>
</ElCol>
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.endVal') }}</span>
<ElInputNumber v-model="endVal" :min="1" />
</div>
</ElCol>
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.duration') }}</span>
<ElInputNumber v-model="duration" :min="1000" />
</div>
</ElCol>
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.separator') }}</span>
<ElInput v-model="separator" />
</div>
</ElCol>
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.prefix') }}</span>
<ElInput v-model="prefix" />
</div>
</ElCol>
<ElCol :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
<div class="flex mb-20px items-center">
<span class="min-w-90px text-right">{{ t('countToDemo.suffix') }}</span>
<ElInput v-model="suffix" />
</div>
</ElCol>
<ElCol :span="24">
<div class="text-center">
<BaseButton type="primary" @click="start">{{ t('countToDemo.start') }}</BaseButton>
<BaseButton @click="pauseResume">
{{ t('countToDemo.pause') }}/{{ t('countToDemo.resume') }}
</BaseButton>
</div>
</ElCol>
</ElRow>
</ContentWrap>
</template>

View File

@@ -0,0 +1,192 @@
<script setup lang="tsx">
import { Descriptions } from '@/components/Descriptions'
import { useI18n } from '@/hooks/web/useI18n'
import { reactive } from 'vue'
import { Form } from '@/components/Form'
import { ElFormItem, ElInput } from 'element-plus'
import { useValidator } from '@/hooks/web/useValidator'
import { useForm } from '@/hooks/web/useForm'
import { DescriptionsSchema } from '@/components/Descriptions'
const { required } = useValidator()
const { t } = useI18n()
const data = reactive({
username: 'chenkl',
nickName: '梦似花落。',
age: 26,
phone: '13655971xxxx',
email: '502431556@qq.com',
addr: '这是一个很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的地址',
sex: '男',
certy: '3505831994xxxxxxxx'
})
const schema = reactive<DescriptionsSchema[]>([
{
field: 'username',
label: t('descriptionsDemo.username')
},
{
field: 'nickName',
label: t('descriptionsDemo.nickName')
},
{
field: 'phone',
label: t('descriptionsDemo.phone')
},
{
field: 'email',
label: t('descriptionsDemo.email')
},
{
field: 'addr',
label: t('descriptionsDemo.addr'),
span: 24
}
])
const schema2 = reactive<DescriptionsSchema[]>([
{
field: 'username',
label: t('descriptionsDemo.username'),
slots: {
label: (row) => {
return <span class="is-required--item">{row.label}</span>
},
default: () => {
return (
<ElFormItem prop="username">
<ElInput v-model={form.username} />
</ElFormItem>
)
}
}
},
{
field: 'nickName',
label: t('descriptionsDemo.nickName'),
slots: {
label: (row) => {
return <span class="is-required--item">{row.label}</span>
},
default: () => {
return (
<ElFormItem prop="nickName">
<ElInput v-model={form.nickName} />
</ElFormItem>
)
}
}
},
{
field: 'phone',
label: t('descriptionsDemo.phone'),
slots: {
label: (row) => {
return <span class="is-required--item">{row.label}</span>
},
default: () => {
return (
<ElFormItem prop="phone">
<ElInput v-model={form.phone} />
</ElFormItem>
)
}
}
},
{
field: 'email',
label: t('descriptionsDemo.email'),
slots: {
label: (row) => {
return <span class="is-required--item">{row.label}</span>
},
default: () => {
return (
<ElFormItem prop="email">
<ElInput v-model={form.email} />
</ElFormItem>
)
}
}
},
{
field: 'addr',
label: t('descriptionsDemo.addr'),
slots: {
label: (row) => {
return <span class="is-required--item">{row.label}</span>
},
default: () => {
return (
<ElFormItem prop="addr">
<ElInput v-model={form.addr} />
</ElFormItem>
)
}
},
span: 24
}
])
const form = reactive({
username: '',
nickName: '',
phone: '',
email: '',
addr: ''
})
const rules = reactive({
username: [required()],
nickName: [required()],
phone: [required()],
email: [required()],
addr: [required()]
})
const { formRegister, formMethods } = useForm()
const { getElFormExpose } = formMethods
const formValidation = async () => {
const elFormExpose = await getElFormExpose()
elFormExpose?.validate((isValid) => {
console.log(isValid)
})
}
</script>
<template>
<Descriptions
:title="t('descriptionsDemo.descriptions')"
:message="t('descriptionsDemo.descriptionsDes')"
:data="data"
:schema="schema"
/>
<Form is-custom :model="form" :rules="rules" @register="formRegister">
<Descriptions
:title="t('descriptionsDemo.form')"
:data="data"
:schema="schema2"
class="mt-20px"
/>
<div class="text-center mt-10px">
<BaseButton @click="formValidation"> {{ t('formDemo.formValidation') }} </BaseButton>
</div>
</Form>
</template>
<style lang="less" scoped>
:deep(.is-required--item) {
position: relative;
&::before {
margin-right: 4px;
color: var(--el-color-danger);
content: '*';
}
}
</style>

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { Dialog } from '@/components/Dialog'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive } from 'vue'
import { Form, FormSchema } from '@/components/Form'
import { useValidator } from '@/hooks/web/useValidator'
import { getDictOneApi } from '@/api/common'
import { useForm } from '@/hooks/web/useForm'
import Echart from './Echart.vue'
import ResizeDialog from '@/components/Dialog/src/ResizeDialog.vue'
const { required } = useValidator()
const { t } = useI18n()
const dialogVisible = ref(false)
const dialogVisible2 = ref(false)
const dialogVisible3 = ref(false)
const dialogVisible4 = ref(false)
const { formRegister, formMethods } = useForm()
const { getElFormExpose } = formMethods
const schema = reactive<FormSchema[]>([
{
field: 'field1',
label: t('formDemo.input'),
component: 'Input',
formItemProps: {
rules: [required()]
}
},
{
field: 'field2',
label: t('formDemo.select'),
component: 'Select',
// componentProps: {
// options: []
// },
optionApi: async () => {
const res = await getDictOneApi()
return res.data
}
},
{
field: 'field3',
label: t('formDemo.radio'),
component: 'RadioGroup',
componentProps: {
options: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
}
]
}
},
{
field: 'field4',
label: t('formDemo.checkbox'),
component: 'CheckboxGroup',
value: [],
componentProps: {
options: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
}
]
}
},
{
field: 'field5',
component: 'DatePicker',
label: t('formDemo.datePicker'),
componentProps: {
type: 'date'
}
},
{
field: 'field6',
component: 'TimeSelect',
label: t('formDemo.timeSelect')
}
])
const formSubmit = async () => {
const elFormExpose = await getElFormExpose()
elFormExpose?.validate((valid) => {
if (valid) {
console.log('submit success')
} else {
console.log('submit fail')
}
})
}
</script>
<template>
<ContentWrap :title="t('dialogDemo.dialog')" :message="t('dialogDemo.dialogDes')">
<BaseButton type="primary" @click="dialogVisible = !dialogVisible">
{{ t('dialogDemo.open') }}
</BaseButton>
<BaseButton type="primary" @click="dialogVisible2 = !dialogVisible2">
{{ t('dialogDemo.combineWithForm') }}
</BaseButton>
<Dialog v-model="dialogVisible" :title="t('dialogDemo.dialog')">
<Echart />
<template #footer>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
<Dialog v-model="dialogVisible2" :title="t('dialogDemo.dialog')">
<Form :schema="schema" @register="formRegister" />
<template #footer>
<BaseButton type="primary" @click="formSubmit">{{ t('dialogDemo.submit') }}</BaseButton>
<BaseButton @click="dialogVisible2 = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</ContentWrap>
<ContentWrap
class="mt-10px"
:title="t('dialogDemo.resizeDialog')"
:message="t('dialogDemo.dialogDes')"
>
<BaseButton type="primary" @click="dialogVisible3 = !dialogVisible3">
{{ t('dialogDemo.open') }}
</BaseButton>
<BaseButton type="primary" @click="dialogVisible4 = !dialogVisible4">
{{ t('dialogDemo.combineWithForm') }}
</BaseButton>
<ResizeDialog v-model="dialogVisible3" :title="t('dialogDemo.dialog')">
<Echart />
<template #footer>
<BaseButton @click="dialogVisible3 = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</ResizeDialog>
<ResizeDialog v-model="dialogVisible4" :title="t('dialogDemo.dialog')">
<Form :schema="schema" @register="formRegister" />
<template #footer>
<BaseButton type="primary" @click="formSubmit">{{ t('dialogDemo.submit') }}</BaseButton>
<BaseButton @click="dialogVisible4 = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</ResizeDialog>
</ContentWrap>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { pieOptions, barOptions, lineOptions, wordOptions } from '@/views/Dashboard/echarts-data'
import { Echart } from '@/components/Echart'
import { ElRow, ElCol, ElCard } from 'element-plus'
const { t } = useI18n()
</script>
<template>
<ContentWrap :title="t('echartDemo.echart')" :message="t('echartDemo.echartDes')">
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<Echart :options="pieOptions" :height="300" />
</ElCard>
</ElCol>
<ElCol :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<Echart :options="barOptions" :height="300" />
</ElCard>
</ElCol>
<ElCol :span="24">
<ElCard shadow="hover" class="mb-20px">
<Echart :options="lineOptions" :height="350" />
</ElCard>
</ElCol>
<ElCol :span="24">
<ElCard shadow="hover" class="mb-20px">
<Echart :options="wordOptions as any" :height="300" />
</ElCard>
</ElCol>
</ElRow>
</ContentWrap>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="tsx">
import { CodeEditor } from '@/components/CodeEditor'
import { useI18n } from '@/hooks/web/useI18n'
import { ContentWrap } from '@/components/ContentWrap'
import { ref } from 'vue'
import { BaseButton } from '@/components/Button'
import { ElDivider } from 'element-plus'
const content = ref(
'public class HelloWorld {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}'
)
const { t } = useI18n()
const MonacoEditRef = ref<InstanceType<typeof CodeEditor>>()
</script>
<template>
<ContentWrap :title="t('richText.codeEditor')" :message="t('richText.codeEditorDes')">
<BaseButton @click="console.log(content)">控制台打印内容</BaseButton>
<ElDivider />
<div class="edit-container h-60vh">
<CodeEditor ref="MonacoEditRef" v-model="content" language="java" />
</div>
</ContentWrap>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { Editor, EditorExpose } from '@/components/Editor'
import { useI18n } from '@/hooks/web/useI18n'
import { IDomEditor } from '@wangeditor/editor'
import { ref, onMounted, unref } from 'vue'
const { t } = useI18n()
const change = (editor: IDomEditor) => {
console.log(editor.getHtml())
}
const editorRef = ref<typeof Editor & EditorExpose>()
const defaultHtml = ref('')
onMounted(async () => {
const editor = await unref(editorRef)?.getEditorRef()
console.log(editor)
})
setTimeout(() => {
defaultHtml.value = '<p>hello <strong>world</strong></p>'
}, 3000)
</script>
<template>
<ContentWrap :title="t('richText.richText')" :message="t('richText.richTextDes')">
<Editor v-model="defaultHtml" ref="editorRef" @change="change" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { JsonEditor } from '@/components/JsonEditor'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, watch } from 'vue'
const { t } = useI18n()
const defaultData = ref({
title: '标题',
content: '内容'
})
watch(
() => defaultData.value,
(val) => {
console.log(val)
},
{
deep: true
}
)
setTimeout(() => {
defaultData.value = {
title: '异步标题',
content: '异步内容'
}
}, 4000)
</script>
<template>
<ContentWrap :title="t('richText.jsonEditor')" :message="t('richText.jsonEditorDes')">
<JsonEditor v-model="defaultData" />
</ContentWrap>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,470 @@
<script setup lang="tsx">
import { Form, FormSchema } from '@/components/Form'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { useForm } from '@/hooks/web/useForm'
import { reactive, unref, ref } from 'vue'
import { ElInput, FormItemProp, ComponentSize, ElMessage, ElMessageBox } from 'element-plus'
import { useValidator } from '@/hooks/web/useValidator'
import { getDictOneApi } from '@/api/common'
import { BaseButton } from '@/components/Button'
const { required } = useValidator()
const { t } = useI18n()
const treeSelectData = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1'
}
]
}
]
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1'
}
]
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1'
}
]
}
]
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1'
}
]
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1'
}
]
}
]
}
]
// 模拟远程加载
const getTreeSelectData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(treeSelectData)
}, 3000)
})
}
const schema = reactive<FormSchema[]>([
{
field: 'field1',
label: t('formDemo.input'),
component: 'Input',
formItemProps: {
rules: [required()]
}
},
{
field: 'field2',
label: t('formDemo.select'),
component: 'Select',
componentProps: {
options: [
{
label: 'option1',
value: '1'
},
{
label: 'option2',
value: '2'
}
]
},
formItemProps: {
rules: [required()]
}
},
{
field: 'field3',
label: t('formDemo.radio'),
component: 'RadioGroup',
hidden: true,
value: '1',
componentProps: {
options: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
}
]
}
},
{
field: 'field4',
label: t('formDemo.checkbox'),
component: 'CheckboxGroup',
value: ['1'],
remove: true,
componentProps: {
options: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
},
{
label: 'option-3',
value: '3'
}
]
}
},
{
field: 'field5',
component: 'DatePicker',
label: t('formDemo.datePicker'),
componentProps: {
type: 'date'
}
},
{
field: 'field6',
component: 'TimeSelect',
label: t('formDemo.timeSelect')
},
{
field: 'field7',
label: `${t('formDemo.treeSelect')}`,
component: 'TreeSelect',
// 远程加载option
optionApi: async () => {
const res = await getTreeSelectData()
return res
}
},
{
field: 'field8',
component: 'Upload',
label: `${t('formDemo.default')}`,
componentProps: {
limit: 3,
action: 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15',
// fileList: ,
multiple: true,
onPreview: (uploadFile) => {
console.log(uploadFile)
},
onRemove: (file) => {
console.log(file)
},
beforeRemove: (uploadFile) => {
return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
() => true,
() => false
)
},
onExceed: (files, uploadFiles) => {
ElMessage.warning(
`The limit is 3, you selected ${files.length} files this time, add up to ${
files.length + uploadFiles.length
} totally`
)
},
slots: {
default: () => <BaseButton type="primary">Click to upload</BaseButton>,
tip: () => <div class="el-upload__tip">jpg/png files with a size less than 500KB.</div>
}
}
}
])
const { formRegister, formMethods } = useForm()
const {
setProps,
delSchema,
addSchema,
setValues,
setSchema,
getComponentExpose,
getFormItemExpose,
getElFormExpose,
getFormData
} = formMethods
const changeLabelWidth = (width: number | string) => {
setProps({
labelWidth: width
})
}
const changeSize = (size: ComponentSize) => {
setProps({
size
})
}
const changeDisabled = (bool: boolean) => {
setProps({
disabled: bool
})
}
const changeSchema = (del: boolean) => {
if (del) {
delSchema('field2')
} else if (!del && schema[1].field !== 'field2') {
addSchema(
{
field: 'field2',
label: t('formDemo.select'),
component: 'Select',
componentProps: {
options: [
{
label: 'option1',
value: '1'
},
{
label: 'option2',
value: '2'
}
]
}
},
1
)
}
}
const setValue = async (reset: boolean) => {
const elFormExpose = await getElFormExpose()
if (reset) {
elFormExpose?.resetFields()
} else {
setValues({
field1: 'field1',
field2: '2',
field3: '2',
field4: ['1', '3'],
field5: '2022-01-27',
field6: '17:00',
field8: [
{
name: 'element-plus-logo.svg',
url: 'https://element-plus.org/images/element-plus-logo.svg'
},
{
name: 'element-plus-logo2.svg',
url: 'https://element-plus.org/images/element-plus-logo.svg'
}
]
})
const formData = await getFormData()
console.log(formData)
}
}
const index = ref(7)
const setLabel = () => {
setSchema([
{
field: 'field2',
path: 'label',
value: `${t('formDemo.select')} ${index.value}`
},
{
field: 'field2',
path: 'componentProps.options',
value: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
},
{
label: 'option-3',
value: '3'
}
]
}
])
index.value++
}
const addItem = () => {
if (unref(index) % 2 === 0) {
addSchema({
field: `field${unref(index)}`,
label: `${t('formDemo.input')}${unref(index)}`,
component: 'Input'
})
} else {
addSchema(
{
field: `field${unref(index)}`,
label: `${t('formDemo.input')}${unref(index)}`,
component: 'Input'
},
unref(index)
)
}
index.value++
}
const formValidation = async () => {
const elFormExpose = await getElFormExpose()
elFormExpose?.validate((isValid) => {
console.log(isValid)
})
}
const verifyReset = async () => {
const elFormExpose = await getElFormExpose()
elFormExpose?.resetFields()
}
const getDictOne = async () => {
const res = await getDictOneApi()
if (res) {
setSchema([
{
field: 'field2',
path: 'componentProps.options',
value: res.data
}
])
}
}
const inoutFocus = async () => {
const inputEl: ComponentRef<typeof ElInput> = await getComponentExpose('field1')
inputEl?.focus()
}
const inoutValidation = async () => {
const formItem = await getFormItemExpose('field1')
const inputEl: ComponentRef<typeof ElInput> = await getComponentExpose('field1')
inputEl?.focus()
formItem?.validate('focus', (val: boolean) => {
console.log(val)
})
}
const formValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
console.log(prop, isValid, message)
}
setTimeout(async () => {
const formData = await getFormData()
console.log(formData)
}, 2000)
const getData = async () => {
const formData = await getFormData()
console.log(formData)
}
</script>
<template>
<ContentWrap :title="`UseForm ${t('formDemo.operate')}`" style="margin-bottom: 20px">
<BaseButton @click="changeLabelWidth(150)">{{ t('formDemo.change') }} labelWidth</BaseButton>
<BaseButton @click="changeLabelWidth('auto')"
>{{ t('formDemo.restore') }} labelWidth</BaseButton
>
<BaseButton @click="changeSize('large')">{{ t('formDemo.change') }} size</BaseButton>
<BaseButton @click="changeSize('default')">{{ t('formDemo.restore') }} size</BaseButton>
<BaseButton @click="changeDisabled(true)">{{ t('formDemo.disabled') }}</BaseButton>
<BaseButton @click="changeDisabled(false)">{{ t('formDemo.disablement') }}</BaseButton>
<BaseButton @click="changeSchema(true)">
{{ t('formDemo.delete') }} {{ t('formDemo.select') }}
</BaseButton>
<BaseButton @click="changeSchema(false)">
{{ t('formDemo.add') }} {{ t('formDemo.select') }}
</BaseButton>
<BaseButton @click="setValue(false)">{{ t('formDemo.setValue') }}</BaseButton>
<BaseButton @click="setValue(true)">{{ t('formDemo.resetValue') }}</BaseButton>
<BaseButton @click="setLabel">
{{ t('formDemo.set') }} {{ t('formDemo.select') }} label
</BaseButton>
<BaseButton @click="addItem"> {{ t('formDemo.add') }} {{ t('formDemo.subitem') }} </BaseButton>
<BaseButton @click="formValidation"> {{ t('formDemo.formValidation') }} </BaseButton>
<BaseButton @click="verifyReset"> {{ t('formDemo.verifyReset') }} </BaseButton>
<BaseButton @click="getDictOne">
{{ `${t('formDemo.select')} ${t('searchDemo.dynamicOptions')}` }}
</BaseButton>
<BaseButton @click="inoutFocus">
{{ `${t('formDemo.input')} ${t('formDemo.focus')}` }}
</BaseButton>
<BaseButton @click="inoutValidation">
{{ `${t('formDemo.input')} ${t('formDemo.formValidation')}` }}
</BaseButton>
<BaseButton @click="getData"> 获取值 </BaseButton>
</ContentWrap>
<ContentWrap :title="`UseForm ${t('formDemo.example')}`">
<Form :schema="schema" @register="formRegister" @validate="formValidate" />
</ContentWrap>
</template>
<style lang="less" scoped>
.el-button {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Highlight } from '@/components/Highlight'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
const keyClick = (key: string) => {
ElMessage.info(key)
}
</script>
<template>
<ContentWrap :title="t('highlightDemo.highlight')">
<Highlight :keys="[t('highlightDemo.keys1'), t('highlightDemo.keys2')]" @click="keyClick">
{{ t('highlightDemo.message') }}
</Highlight>
</ContentWrap>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { IAgree } from '@/components/IAgree'
const { t } = useI18n()
</script>
<template>
<ContentWrap :title="t('router.iAgree')">
<IAgree
:link="[
{
text: '《隐私政策》',
url: 'https://www.baidu.com'
}
]"
text="我同意《隐私政策》"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Infotip } from '@/components/Infotip'
import { useIcon } from '@/hooks/web/useIcon'
const { t } = useI18n()
const keyClick = (key: string) => {
if (key === t('iconDemo.accessAddress')) {
window.open('https://iconify.design/')
}
}
const peoples = useIcon({ icon: 'svg-icon:peoples' })
const money = useIcon({ icon: 'svg-icon:money' })
const aim = useIcon({ icon: 'ep:aim' })
const alarmClock = useIcon({ icon: 'ep:alarm-clock' })
</script>
<template>
<Infotip
:show-index="false"
:title="`${t('iconDemo.recommendedUse')}${t('iconDemo.iconify')}`"
:schema="[
{
label: t('iconDemo.recommendeDes'),
keys: ['Iconify']
},
{
label: t('iconDemo.accessAddress'),
keys: [t('iconDemo.accessAddress')]
}
]"
@click="keyClick"
/>
<ContentWrap :title="t('iconDemo.localIcon')">
<div class="flex justify-between">
<Icon icon="svg-icon:peoples" />
<Icon icon="svg-icon:money" />
<Icon icon="svg-icon:message" />
<Icon icon="svg-icon:shopping" />
</div>
</ContentWrap>
<ContentWrap :title="t('iconDemo.iconify')">
<div class="flex justify-between">
<Icon icon="vi-ep:aim" />
<Icon icon="vi-ep:alarm-clock" />
<Icon icon="vi-ep:baseball" />
<Icon icon="vi-ep:chat-line-round" />
</div>
</ContentWrap>
<ContentWrap title="useIcon">
<div class="flex justify-between">
<BaseButton :icon="peoples">Button</BaseButton>
<BaseButton :icon="money">Button</BaseButton>
<BaseButton :icon="aim">Button</BaseButton>
<BaseButton :icon="alarmClock">Button</BaseButton>
</div>
</ContentWrap>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { IconPicker } from '@/components/IconPicker'
import { ref } from 'vue'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const currentIcon = ref('')
</script>
<template>
<ContentWrap :title="t('router.iconPicker')">
<IconPicker v-model="currentIcon" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { ImageCropping } from '@/components/ImageCropping'
import { ref, unref } from 'vue'
import { ElInput, ElDivider } from 'element-plus'
const cropperExpose = ref<InstanceType<typeof ImageCropping>>()
const base64 = ref('')
const getBase64 = () => {
base64.value = unref(cropperExpose)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
}
const cropperExpose2 = ref<InstanceType<typeof ImageCropping>>()
const base642 = ref('')
const getBase642 = () => {
base642.value = unref(cropperExpose)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
}
</script>
<template>
<ContentWrap title="图片裁剪">
<BaseButton type="primary" class="mb-20px" @click="getBase64">裁剪</BaseButton>
<ElInput v-model="base64" class="mb-20px" type="textarea" />
<ImageCropping
ref="cropperExpose"
image-url="https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*"
/>
<ElDivider />
<BaseButton type="primary" class="mb-20px" @click="getBase642">裁剪</BaseButton>
<ElInput v-model="base642" class="mb-20px" type="textarea" />
<ImageCropping
ref="cropperExpose2"
:show-actions="false"
box-width="100%"
:show-result="false"
image-url="https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { createImageViewer } from '@/components/ImageViewer'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const open = () => {
createImageViewer({
urlList: [
'https://images6.alphacoders.com/657/thumbbig-657194.webp',
'https://images3.alphacoders.com/677/thumbbig-677688.webp',
'https://images4.alphacoders.com/200/thumbbig-200966.webp',
'https://images5.alphacoders.com/657/thumbbig-657248.webp',
'https://images3.alphacoders.com/679/thumbbig-679917.webp',
'https://images3.alphacoders.com/737/thumbbig-73785.webp'
]
})
}
</script>
<template>
<ContentWrap
:title="t('imageViewerDemo.imageViewer')"
:message="t('imageViewerDemo.imageViewerDes')"
>
<BaseButton type="primary" @click="open">{{ t('imageViewerDemo.open') }}</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Infotip } from '@/components/Infotip'
const { t } = useI18n()
const keyClick = (key: string) => {
if (key === t('iconDemo.accessAddress')) {
window.open('https://iconify.design/')
}
}
</script>
<template>
<ContentWrap :title="t('infotipDemo.infotip')" :message="t('infotipDemo.infotipDes')">
<Infotip
:show-index="false"
:title="`${t('iconDemo.recommendedUse')}${t('iconDemo.iconify')}`"
:schema="[
{
label: t('iconDemo.recommendeDes'),
keys: ['Iconify']
},
{
label: t('iconDemo.accessAddress'),
keys: [t('iconDemo.accessAddress')]
}
]"
@click="keyClick"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { InputPassword } from '@/components/InputPassword'
import { ref } from 'vue'
const { t } = useI18n()
const password = ref('')
</script>
<template>
<ContentWrap
:title="t('inputPasswordDemo.title')"
:message="t('inputPasswordDemo.inputPasswordDes')"
>
<InputPassword v-model="password" class="mb-20px" />
<InputPassword v-model="password" strength />
<InputPassword v-model="password" strength disabled class="mt-20px" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { Qrcode } from '@/components/Qrcode'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { computed, ref, unref } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { ElRow, ElCard, ElCol, ElMessage } from 'element-plus'
// @ts-ignore
import logoImg from '@/assets/imgs/logo.png'
const appStore = useAppStore()
const { t } = useI18n()
const title = computed(() => appStore.getTitle)
const asyncTitle = ref('')
setTimeout(() => {
asyncTitle.value = unref(title)
}, 3000)
const codeClick = () => {
ElMessage.info(t('qrcodeDemo.click'))
}
const disabledClick = () => {
ElMessage.info(t('qrcodeDemo.invalid'))
}
</script>
<template>
<ContentWrap :title="t('qrcodeDemo.qrcode')" :message="t('qrcodeDemo.qrcodeDes')">
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.basicUsage') }}</div>
<Qrcode :text="title" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.imgTag') }}</div>
<Qrcode :text="title" tag="img" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.style') }}</div>
<Qrcode
:text="title"
:options="{
color: {
dark: '#55D187',
light: '#2d8cf0'
}
}"
/>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.click') }}</div>
<Qrcode :text="title" @click="codeClick" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.asynchronousContent') }}</div>
<Qrcode :text="asyncTitle" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.invalid') }}</div>
<Qrcode :text="title" disabled @disabled-click="disabledClick" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.logoConfig') }}</div>
<Qrcode :text="title" :logo="logoImg" />
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.logoStyle') }}</div>
<Qrcode
:text="title"
:logo="{
src: logoImg,
logoSize: 0.2,
borderSize: 0.05,
borderRadius: 50,
bgColor: 'blue'
}"
/>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-10px text-center">
<div class="font-bold">{{ t('qrcodeDemo.size') }}</div>
<Qrcode :text="title" :width="100" />
</ElCard>
</ElCol>
</ElRow>
</ContentWrap>
</template>

View File

@@ -0,0 +1,363 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Search } from '@/components/Search'
import { reactive, ref, unref } from 'vue'
import { getDictOneApi } from '@/api/common'
import { FormSchema } from '@/components/Form'
import { useSearch } from '@/hooks/web/useSearch'
const { t } = useI18n()
const { searchRegister, searchMethods } = useSearch()
const { setSchema, setProps, setValues, getFormData } = searchMethods
const treeSelectData = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1'
}
]
}
]
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1'
}
]
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1'
}
]
}
]
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1'
}
]
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1'
}
]
}
]
}
]
// 模拟远程加载
const getTreeSelectData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(treeSelectData)
}, 3000)
})
}
const schema = reactive<FormSchema[]>([
{
field: 'field1',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field2',
label: t('formDemo.select'),
component: 'Select',
componentProps: {
options: [
{
label: 'option1',
value: '1'
},
{
label: 'option2',
value: '2'
}
],
on: {
change: (value: string) => {
console.log(value)
}
}
}
},
{
field: 'field3',
label: t('formDemo.radio'),
component: 'RadioGroup',
componentProps: {
options: [
{
label: 'option-1',
value: '1'
},
{
label: 'option-2',
value: '2'
}
]
}
},
{
field: 'field5',
component: 'DatePicker',
label: t('formDemo.datePicker'),
componentProps: {
type: 'date'
}
},
{
field: 'field6',
component: 'TimeSelect',
label: t('formDemo.timeSelect')
},
{
field: 'field8',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field9',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field10',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field11',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field12',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field13',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field14',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field15',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field16',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field17',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field18',
label: t('formDemo.input'),
component: 'Input'
},
{
field: 'field19',
label: `${t('formDemo.treeSelect')}`,
component: 'TreeSelect',
// 远程加载option
optionApi: async () => {
const res = await getTreeSelectData()
return res
}
}
])
const isGrid = ref(false)
const changeGrid = (grid: boolean) => {
setProps({
isCol: grid
})
// isGrid.value = grid
}
const layout = ref('inline')
const changeLayout = () => {
layout.value = unref(layout) === 'inline' ? 'bottom' : 'inline'
}
const buttonPosition = ref('left')
const changePosition = (position: string) => {
layout.value = 'bottom'
buttonPosition.value = position
}
const getDictOne = async () => {
const res = await getDictOneApi()
if (res) {
setSchema([
{
field: 'field2',
path: 'componentProps.options',
value: res.data
}
])
}
}
const handleSearch = async (data: any) => {
const formData = await getFormData()
console.log(formData)
console.log(data)
}
const delRadio = () => {
setSchema([
{
field: 'field3',
path: 'remove',
value: true
}
])
}
const restoreRadio = () => {
setSchema([
{
field: 'field3',
path: 'remove',
value: false
}
])
}
const setValue = () => {
setValues({
field1: 'Joy'
})
}
const searchLoading = ref(false)
const changeSearchLoading = () => {
searchLoading.value = true
setTimeout(() => {
searchLoading.value = false
}, 2000)
}
const resetLoading = ref(false)
const changeResetLoading = () => {
resetLoading.value = true
setTimeout(() => {
resetLoading.value = false
}, 2000)
}
</script>
<template>
<ContentWrap
:title="`${t('searchDemo.search')} ${t('searchDemo.operate')}`"
style="margin-bottom: 20px"
>
<BaseButton @click="changeGrid(true)">{{ t('searchDemo.grid') }}</BaseButton>
<BaseButton @click="changeGrid(false)">
{{ t('searchDemo.restore') }} {{ t('searchDemo.grid') }}
</BaseButton>
<BaseButton @click="changeLayout">
{{ t('searchDemo.button') }} {{ t('searchDemo.position') }}
</BaseButton>
<BaseButton @click="changePosition('left')">
{{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.left') }}
</BaseButton>
<BaseButton @click="changePosition('center')">
{{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.center') }}
</BaseButton>
<BaseButton @click="changePosition('right')">
{{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.right') }}
</BaseButton>
<BaseButton @click="getDictOne">
{{ t('formDemo.select') }} {{ t('searchDemo.dynamicOptions') }}
</BaseButton>
<BaseButton @click="delRadio">{{ t('searchDemo.deleteRadio') }}</BaseButton>
<BaseButton @click="restoreRadio">{{ t('searchDemo.restoreRadio') }}</BaseButton>
<BaseButton @click="setValue">{{ t('formDemo.setValue') }}</BaseButton>
<BaseButton @click="changeSearchLoading">
{{ t('searchDemo.search') }} {{ t('searchDemo.loading') }}
</BaseButton>
<BaseButton @click="changeResetLoading">
{{ t('searchDemo.reset') }} {{ t('searchDemo.loading') }}
</BaseButton>
</ContentWrap>
<ContentWrap :title="t('searchDemo.search')" :message="t('searchDemo.searchDes')">
<Search
:schema="schema"
:is-col="isGrid"
:layout="layout"
:button-position="buttonPosition"
:search-loading="searchLoading"
:reset-loading="resetLoading"
show-expand
expand-field="field6"
@search="handleSearch"
@reset="handleSearch"
@register="searchRegister"
/>
</ContentWrap>
</template>
<style lang="less" scoped>
.el-button {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,80 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table } from '@/components/Table'
import { getCardTableListApi } from '@/api/table'
import { ref } from 'vue'
import { ElLink, ElDivider } from 'element-plus'
interface Params {
pageIndex?: number
pageSize?: number
}
const { t } = useI18n()
const loading = ref(true)
const tableDataList = ref<any[]>([])
const getTableList = async (params?: Params) => {
const res = await getCardTableListApi(
params || {
pageIndex: 1,
pageSize: 10
}
)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
tableDataList.value = res.data.list
}
}
getTableList()
const actionClick = (row?: any) => {
console.log(row)
}
</script>
<template>
<ContentWrap :title="t('tableDemo.cardTable')">
<Table
:columns="[]"
:data="tableDataList"
:loading="loading"
custom-content
:card-wrap-style="{
width: '200px',
marginBottom: '20px',
marginRight: '20px'
}"
>
<template #content="row">
<div class="flex cursor-pointer">
<div class="pr-16px">
<img :src="row.logo" class="w-48px h-48px rounded-[50%]" alt="" />
</div>
<div>
<div class="mb-12px font-700 font-size-16px">{{ row.name }}</div>
<div class="line-clamp-3 font-size-12px">{{ row.desc }}</div>
</div>
</div>
</template>
<template #content-footer="item">
<div class="flex justify-center items-center">
<div class="flex-1 text-center" @click="() => actionClick(item)">
<ElLink :underline="false">操作一</ElLink>
</div>
<ElDivider direction="vertical" />
<div class="flex-1 text-center" @click="() => actionClick(item)">
<ElLink :underline="false">操作二</ElLink>
</div>
</div>
</template>
</Table>
</ContentWrap>
</template>

View File

@@ -0,0 +1,105 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { getTableListApi } from '@/api/table'
import { TableData } from '@/api/table/types'
import { ref, h } from 'vue'
import { ElTag } from 'element-plus'
import { BaseButton } from '@/components/Button'
interface Params {
pageIndex?: number
pageSize?: number
}
const { t } = useI18n()
const columns: TableColumn[] = [
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime'),
sortable: true
},
{
field: 'importance',
label: t('tableDemo.importance'),
formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
return h(
ElTag,
{
type: cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'
},
() =>
cellValue === 1
? t('tableDemo.important')
: cellValue === 2
? t('tableDemo.good')
: t('tableDemo.commonly')
)
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
},
{
field: 'action',
label: t('tableDemo.action'),
slots: {
default: (data) => {
return (
<BaseButton type="primary" onClick={() => actionFn(data)}>
{t('tableDemo.action')}
</BaseButton>
)
}
}
}
]
const loading = ref(true)
const tableDataList = ref<TableData[]>([])
const getTableList = async (params?: Params) => {
const res = await getTableListApi(
params || {
pageIndex: 1,
pageSize: 10
}
)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
tableDataList.value = res.data.list
}
}
getTableList()
const actionFn = (data: any) => {
console.log(data)
}
</script>
<template>
<ContentWrap :title="t('tableDemo.table')" :message="t('tableDemo.tableDes')">
<Table
:columns="columns"
:data="tableDataList"
:loading="loading"
:defaultSort="{ prop: 'display_time', order: 'descending' }"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { getTableListApi } from '@/api/table'
import { TableData } from '@/api/table/types'
import { ref } from 'vue'
import { ElTag } from 'element-plus'
interface Params {
pageIndex?: number
pageSize?: number
}
const { t } = useI18n()
const columns: TableColumn[] = [
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'image_uri',
label: t('tableDemo.preview')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime')
},
{
field: 'importance',
label: t('tableDemo.importance'),
formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
return (
<ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>
{cellValue === 1
? t('tableDemo.important')
: cellValue === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
}
]
const loading = ref(true)
const tableDataList = ref<TableData[]>([])
const getTableList = async (params?: Params) => {
const res = await getTableListApi(
params || {
pageIndex: 1,
pageSize: 10
}
)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
tableDataList.value = res.data.list
}
}
getTableList()
</script>
<template>
<ContentWrap :title="t('router.PicturePreview')">
<Table
:columns="columns"
:data="tableDataList"
:loading="loading"
:image-preview="['image_uri']"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { getTableListApi } from '@/api/table'
import { TableData } from '@/api/table/types'
import { ref } from 'vue'
interface Params {
pageIndex?: number
pageSize?: number
}
const { t } = useI18n()
const columns: TableColumn[] = [
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'video_uri',
label: t('tableDemo.videoPreview')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime')
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
}
]
const loading = ref(true)
const tableDataList = ref<TableData[]>([])
const getTableList = async (params?: Params) => {
const res = await getTableListApi(
params || {
pageIndex: 1,
pageSize: 10
}
)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
tableDataList.value = res.data.list
}
}
getTableList()
</script>
<template>
<ContentWrap :title="t('router.PicturePreview')">
<Table
:columns="columns"
:data="tableDataList"
:loading="loading"
:video-preview="['image_uri', 'video_uri']"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,117 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { getTreeTableListApi } from '@/api/table'
import { reactive, unref } from 'vue'
import { ElTag } from 'element-plus'
import { useTable } from '@/hooks/web/useTable'
import { BaseButton } from '@/components/Button'
const { tableRegister, tableState } = useTable({
fetchDataApi: async () => {
const { currentPage, pageSize } = tableState
const res = await getTreeTableListApi({
pageIndex: unref(currentPage),
pageSize: unref(pageSize)
})
return {
list: res.data.list,
total: res.data.total
}
}
})
const { loading, dataList, total, currentPage, pageSize } = tableState
const { t } = useI18n()
const columns = reactive<TableColumn[]>([
{
field: 'selection',
type: 'selection'
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index'
},
{
field: 'content',
label: t('tableDemo.header'),
children: [
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime')
},
{
field: 'importance',
label: t('tableDemo.importance'),
formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
return (
<ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>
{cellValue === 1
? t('tableDemo.important')
: cellValue === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
}
]
},
{
field: 'action',
label: t('tableDemo.action'),
slots: {
default: (data) => {
return (
<BaseButton type="primary" onClick={() => actionFn(data)}>
{t('tableDemo.action')}
</BaseButton>
)
}
}
}
])
const actionFn = (data) => {
console.log(data)
}
</script>
<template>
<ContentWrap :title="`${t('router.treeTable')} ${t('tableDemo.example')}`">
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"
:columns="columns"
:data="dataList"
row-key="id"
:loading="loading"
sortable
:pagination="{
total: total
}"
@register="tableRegister"
/>
</ContentWrap>
</template>
<style lang="less" scoped>
.el-button {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,283 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn, TableSlotDefault } from '@/components/Table'
import { getTableListApi } from '@/api/table'
import { ref, reactive, unref, onMounted } from 'vue'
import { ElTag } from 'element-plus'
import { useTable } from '@/hooks/web/useTable'
import { BaseButton } from '@/components/Button'
const { tableRegister, tableMethods, tableState } = useTable({
fetchDataApi: async () => {
const { currentPage, pageSize } = tableState
const res = await getTableListApi({
pageIndex: unref(currentPage),
pageSize: unref(pageSize)
})
return {
list: res.data.list,
total: res.data.total
}
}
})
const { loading, dataList, total, currentPage, pageSize } = tableState
const { setProps, setColumn, getElTableExpose, addColumn, delColumn, refresh } = tableMethods
const { t } = useI18n()
const columns = reactive<TableColumn[]>([])
onMounted(() => {
setTimeout(() => {
setProps({
columns: [
{
field: 'expand',
type: 'expand',
slots: {
default: (data: TableSlotDefault) => {
const { row } = data
return (
<div class="ml-30px">
<div>
{t('tableDemo.title')}{row.title}
</div>
<div>
{t('tableDemo.author')}{row.author}
</div>
<div>
{t('tableDemo.displayTime')}{row.display_time}
</div>
</div>
)
}
}
},
{
field: 'selection',
type: 'selection'
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index'
},
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime')
},
{
field: 'importance',
label: t('tableDemo.importance'),
formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
return (
<ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>
{cellValue === 1
? t('tableDemo.important')
: cellValue === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
},
{
field: 'action',
label: t('tableDemo.action'),
slots: {
default: (data) => {
return (
<BaseButton type="primary" onClick={() => actionFn(data)}>
{t('tableDemo.action')}
</BaseButton>
)
}
}
}
]
})
}, 2000)
})
const actionFn = (data: TableSlotDefault) => {
console.log(data)
}
const canShowPagination = ref(true)
const showPagination = (show: boolean) => {
canShowPagination.value = show
}
const reserveIndex = (custom: boolean) => {
setProps({
reserveIndex: custom
})
}
const showSelections = (show: boolean) => {
setColumn([
{
field: 'selection',
path: 'hidden',
value: !show
}
])
}
const index = ref(1)
const changeTitle = () => {
setColumn([
{
field: 'title',
path: 'label',
value: `${t('tableDemo.title')}${unref(index)}`
}
])
index.value++
}
const showExpandedRows = (show: boolean) => {
setColumn([
{
field: 'expand',
path: 'hidden',
value: !show
}
])
}
const selectAllNone = async () => {
const elTableRef = await getElTableExpose()
elTableRef?.toggleAllSelection()
}
const showAction = ref(true)
const delOrAddAction = () => {
if (unref(showAction)) {
delColumn('action')
showAction.value = false
} else {
addColumn({
field: 'action',
label: t('tableDemo.action'),
slots: {
default: (data) => {
return (
<BaseButton type="primary" onClick={() => actionFn(data)}>
{t('tableDemo.action')}
</BaseButton>
)
}
}
})
showAction.value = true
}
}
const showStripe = ref(false)
const showOrHiddenStripe = () => {
setProps({
stripe: !unref(showStripe)
})
showStripe.value = !unref(showStripe)
}
const height = ref<string | number>('auto')
const fixedHeaderOrAuto = () => {
if (unref(height) === 'auto') {
setProps({
height: 300
})
height.value = 300
} else {
setProps({
height: 'auto'
})
height.value = 'auto'
}
}
const getSelections = async () => {
const elTableRef = await getElTableExpose()
const selections = elTableRef?.getSelectionRows()
console.log(selections)
}
</script>
<template>
<ContentWrap :title="`UseTable ${t('tableDemo.operate')}`" style="margin-bottom: 20px">
<BaseButton @click="showPagination(true)">
{{ t('tableDemo.show') }} {{ t('tableDemo.pagination') }}
</BaseButton>
<BaseButton @click="showPagination(false)">
{{ t('tableDemo.hidden') }} {{ t('tableDemo.pagination') }}
</BaseButton>
<BaseButton @click="reserveIndex(true)">{{ t('tableDemo.reserveIndex') }}</BaseButton>
<BaseButton @click="reserveIndex(false)">{{ t('tableDemo.restoreIndex') }}</BaseButton>
<BaseButton @click="showSelections(true)">{{ t('tableDemo.showSelections') }}</BaseButton>
<BaseButton @click="showSelections(false)">{{ t('tableDemo.hiddenSelections') }}</BaseButton>
<BaseButton @click="changeTitle">{{ t('tableDemo.changeTitle') }}</BaseButton>
<BaseButton @click="showExpandedRows(true)">{{ t('tableDemo.showExpandedRows') }}</BaseButton>
<BaseButton @click="showExpandedRows(false)">{{
t('tableDemo.hiddenExpandedRows')
}}</BaseButton>
<BaseButton @click="selectAllNone">{{ t('tableDemo.selectAllNone') }}</BaseButton>
<BaseButton @click="delOrAddAction">{{ t('tableDemo.delOrAddAction') }}</BaseButton>
<BaseButton @click="showOrHiddenStripe">{{ t('tableDemo.showOrHiddenStripe') }}</BaseButton>
<BaseButton @click="fixedHeaderOrAuto">{{ t('tableDemo.fixedHeaderOrAuto') }}</BaseButton>
<BaseButton @click="getSelections">{{ t('tableDemo.getSelections') }}</BaseButton>
<!-- <BaseButton @click="showOrHiddenSortable">{{ t('tableDemo.showOrHiddenSortable') }}</BaseButton> -->
</ContentWrap>
<ContentWrap :title="`UseTable ${t('tableDemo.example')}`">
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"
showAction
showSummary
default-expand-all
:columns="columns"
row-key="id"
:data="dataList"
:loading="loading"
:pagination="
canShowPagination
? {
total: total
}
: undefined
"
@register="tableRegister"
@refresh="refresh"
/>
</ContentWrap>
</template>
<style lang="less" scoped>
.el-button {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,252 @@
<script setup lang="tsx">
import { Icon } from '@/components/Icon'
import { Tree } from '@/components/Tree'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref } from 'vue'
const { t } = useI18n()
const treeData = ref([
{
id: 1,
name: '北京',
children: [
{
id: 5,
name: '朝阳',
children: [
{
id: 17,
name: '双塔',
children: []
},
{
id: 18,
name: '龙城',
children: []
}
]
},
{
id: 6,
name: '丰台',
children: [
{
id: 19,
name: '新村',
children: []
},
{
id: 20,
name: '大红门',
children: []
},
{
id: 21,
name: '长辛店',
children: [
{
id: 22,
name: '东山坡',
children: []
},
{
id: 23,
name: '北关',
children: []
},
{
id: 24,
name: '光明里',
children: []
},
{
id: 25,
name: '赵辛店',
children: []
},
{
id: 26,
name: '西峰寺',
children: []
}
]
}
]
},
{
id: 7,
name: '海淀',
children: []
},
{
id: 8,
name: '房山',
children: []
},
{
id: 10,
name: '顺义',
children: []
}
]
},
{
id: 2,
name: '上海',
children: [
{
id: 11,
name: '黄埔',
children: []
},
{
id: 12,
name: '徐汇',
children: []
}
]
},
{
id: 3,
name: '广州',
children: [
{
id: 13,
name: '荔湾',
children: []
},
{
id: 14,
name: '白云',
children: []
},
{
id: 15,
name: '越秀',
children: []
},
{
id: 16,
name: '南沙',
children: []
}
]
}
])
const handleNodeClick = (data: any) => {
console.log('Node clicked:', data)
}
const addOrg = (node: any) => {
ElMessageBox.prompt('请输入分组名称', '添加子分组', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '分组名称不能为空'
}).then(({ value }) => {
node.children.push({
id: node.children.length + 1,
name: value,
children: []
})
ElMessage.success('添加成功')
})
}
const editOrg = (node: any) => {
ElMessageBox.prompt('请输入新的分组名称', '修改分组名称', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: node.name,
inputPattern: /\S/,
inputErrorMessage: '分组名称不能为空'
}).then(({ value }) => {
node.name = value
ElMessage.success('修改成功')
})
}
const deleteOrg = (node: any) => {
ElMessageBox.confirm(`删除 [${node.name}] 分组、下级子分组 <br>是否继续?`, '提示', {
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
}).then(() => {
const id = node.id
// 查找 treeData 中对应的节点,并删除
const deleteNode = (data: any) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
data.splice(i, 1)
return
}
if (data[i].children) {
deleteNode(data[i].children)
}
}
}
deleteNode(treeData.value)
ElMessage.success('删除成功')
})
}
</script>
<template>
<ContentWrap :title="t('treeDemo.treeTitle')" :message="t('qrcodeDemo.qrcodeDes')">
<Tree
:data="treeData"
:tree-props="{
highlightCurrent: true,
nodeKey: 'id',
props: {
children: 'children',
label: 'name'
}
}"
width="300px"
height="400px"
@node-click="handleNodeClick"
>
<!-- 自定义右键菜单 -->
<template #context-menu="{ node }">
<div class="menuItem" @click="addOrg(node)">
<Icon icon="ep:plus" style="color: #1e9fff" />
<span>添加子分组</span>
</div>
<div class="menuItem" @click="editOrg(node)">
<Icon icon="ep:edit-pen" style="color: #1e9fff" />
修改分组名称
</div>
<div class="menuItem" @click="deleteOrg(node)">
<Icon icon="ep:delete" style="color: #1e9fff" />
删除分组及子分组
</div>
</template>
<!-- 自定义节点显示 -->
<!-- <template #render-node="{ node }">
<span v-if="node.isLeaf">[FILE] {{ node.label }}</span>
<span v-else>[FOLDER] {{ node.label }}</span>
</template> -->
</Tree>
</ContentWrap>
</template>
<style lang="less" scoped>
.menuItem {
display: flex;
padding: 2px 10px;
text-align: left;
box-sizing: border-box;
align-items: center; /* 垂直居中 */
gap: 5px; /* 图标和文字之间的间距,可根据需要调整 */
}
.menuItem:hover {
cursor: pointer;
background-color: #eee;
}
</style>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { VideoPlayer, createVideoViewer } from '@/components/VideoPlayer'
import { ContentWrap } from '@/components/ContentWrap'
import { ElDivider } from 'element-plus'
const showVideo = () => {
createVideoViewer({
url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4',
poster: '//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg'
})
}
</script>
<template>
<ContentWrap title="视频播放器">
<VideoPlayer
url="//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4"
poster="//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg"
/>
<ElDivider />
<BaseButton type="primary" @click="showVideo">弹窗展示</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { Waterfall } from '@/components/Waterfall'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import Mock from 'mockjs'
import { ref, unref } from 'vue'
import { toAnyString } from '@/utils'
const data = ref<any>([])
const getList = () => {
const list: any = []
for (let i = 0; i < 20; i++) {
// 随机 100, 500 之间的整数
const height = Mock.Random.integer(100, 500)
const width = Mock.Random.integer(100, 500)
list.push(
Mock.mock({
width,
height,
id: toAnyString(),
// http更换为https
image_uri: Mock.Random.image(`${width}x${height}`).replace('http://', 'https://')
})
)
}
data.value = [...unref(data), ...list]
if (unref(data).length >= 60) {
end.value = true
}
}
getList()
const { t } = useI18n()
const loading = ref(false)
const end = ref(false)
const loadMore = () => {
loading.value = true
setTimeout(() => {
getList()
loading.value = false
}, 1000)
}
</script>
<template>
<ContentWrap :title="t('router.waterfall')">
<Waterfall
:data="data"
:loading="loading"
:end="end"
:props="{
src: 'image_uri',
height: 'height'
}"
@load-more="loadMore"
/>
</ContentWrap>
</template>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import PanelGroup from './components/PanelGroup.vue'
import { ElRow, ElCol, ElCard, ElSkeleton } from 'element-plus'
import { Echart } from '@/components/Echart'
import { pieOptions, barOptions, lineOptions } from './echarts-data'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { getLast10DaysPromotionData } from '@/api/turnover'
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { useI18n } from '@/hooks/web/useI18n'
import { useAppStore } from '@/store/modules/app'
const { t } = useI18n()
const loading = ref(true)
const appStore = useAppStore()
const isDark = computed(() => appStore.getIsDark)
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
const getMonthlySales = async () => {
const chartData: any = {
date: [],
expectedData: [],
actualData: []
}
const res = await getLast10DaysPromotionData().catch(() => {})
if (res && res.data) {
for (const item of res.data) {
chartData.date.push(item.date)
chartData.expectedData.push(item.last10DaysTurnover)
chartData.actualData.push(item.last10DaysAnchorInvite)
}
set(lineOptionsData, 'xAxis.data', chartData.date)
set(lineOptionsData, 'series', [
{
name: '日流水',
smooth: true,
type: 'line',
data: chartData.expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: '日邀请',
smooth: true,
type: 'line',
itemStyle: {},
data: chartData.actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
}
/**
* 更新 legend.textStyle
*/
const updateLegendTextStyle = (options) => {
const newTextStyle = {
color: isDark.value ? '#ccc' : '#333'
}
const inactiveColor = isDark.value ? '#abacac' : '#ccc'
set(options, 'title.textStyle', newTextStyle)
if (options !== barOptionsData) {
set(options, 'legend.textStyle', newTextStyle)
set(options, 'legend.inactiveColor', inactiveColor)
}
options === pieOptionsData && set(options, 'series[0].emptyCircleStyle.color', inactiveColor)
}
const getAllApi = async () => {
await Promise.all([getMonthlySales()])
loading.value = false
}
getAllApi()
// 监听暗黑模式变化并重新更新样式
watch(isDark, () => {
updateLegendTextStyle(pieOptionsData)
updateLegendTextStyle(barOptionsData)
updateLegendTextStyle(lineOptionsData)
})
onMounted(() => {
updateLegendTextStyle(pieOptionsData)
updateLegendTextStyle(barOptionsData)
updateLegendTextStyle(lineOptionsData)
})
</script>
<template>
<PanelGroup />
<ElRow :gutter="20" justify="space-between">
<ElCol :span="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="4">
<Echart :options="lineOptionsData" :height="350" />
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
</template>

View File

@@ -0,0 +1,293 @@
<script setup lang="ts">
import { useTimeAgo } from '@/hooks/web/useTimeAgo'
import { ElRow, ElCol, ElSkeleton, ElCard, ElDivider, ElLink } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive } from 'vue'
import { CountTo } from '@/components/CountTo'
import { formatTime } from '@/utils'
import { Echart } from '@/components/Echart'
import { EChartsOption } from 'echarts'
import { radarOption } from './echarts-data'
import { Highlight } from '@/components/Highlight'
import {
getCountApi,
getProjectApi,
getDynamicApi,
getTeamApi,
getRadarApi
} from '@/api/dashboard/workplace'
import type { WorkplaceTotal, Project, Dynamic, Team } from '@/api/dashboard/workplace/types'
import { set } from 'lodash-es'
const loading = ref(true)
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const getCount = async () => {
const res = await getCountApi().catch(() => {})
if (res) {
totalSate = Object.assign(totalSate, res.data)
}
}
let projects = reactive<Project[]>([])
// 获取项目数
const getProject = async () => {
const res = await getProjectApi().catch(() => {})
if (res) {
projects = Object.assign(projects, res.data)
}
}
// 获取动态
let dynamics = reactive<Dynamic[]>([])
const getDynamic = async () => {
const res = await getDynamicApi().catch(() => {})
if (res) {
dynamics = Object.assign(dynamics, res.data)
}
}
// 获取团队
let team = reactive<Team[]>([])
const getTeam = async () => {
const res = await getTeamApi().catch(() => {})
if (res) {
team = Object.assign(team, res.data)
}
}
// 获取指数
const radarOptionData = reactive<EChartsOption>(radarOption) as EChartsOption
const getRadar = async () => {
const res = await getRadarApi().catch(() => {})
if (res) {
set(
radarOptionData,
'radar.indicator',
res.data.map((v) => {
return {
name: t(v.name),
max: v.max
}
})
)
set(radarOptionData, 'series', [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: res.data.map((v) => v.personal),
name: t('workplace.personal')
},
{
value: res.data.map((v) => v.team),
name: t('workplace.team')
}
]
}
])
}
}
const getAllApi = async () => {
await Promise.all([getCount(), getProject(), getDynamic(), getTeam(), getRadar()])
loading.value = false
}
getAllApi()
const { t } = useI18n()
</script>
<template>
<div>
<ElCard shadow="never">
<ElSkeleton :loading="loading" animated>
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<img
src="@/assets/imgs/avatar.jpg"
alt=""
class="w-70px h-70px rounded-[50%] mr-20px"
/>
<div>
<div class="text-20px">
{{ t('workplace.goodMorning') }}Archer{{ t('workplace.happyDay') }}
</div>
<div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }}20 - 32
</div>
</div>
</div>
</ElCol>
<ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex h-70px items-center justify-end lt-sm:mt-20px">
<div class="px-8px text-right">
<div class="text-14px text-gray-400 mb-20px">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
/>
</div>
<ElDivider direction="vertical" />
<div class="px-8px text-right">
<div class="text-14px text-gray-400 mb-20px">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<ElDivider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="text-14px text-gray-400 mb-20px">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
/>
</div>
</div>
</ElCol>
</ElRow>
</ElSkeleton>
</ElCard>
</div>
<ElRow class="mt-20px" :gutter="20" justify="space-between">
<ElCol :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-20px">
<ElCard shadow="never">
<template #header>
<div class="flex justify-between">
<span>{{ t('workplace.project') }}</span>
<ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
</div>
</template>
<ElSkeleton :loading="loading" animated>
<ElRow>
<ElCol
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="12"
:sm="24"
:xs="24"
>
<ElCard shadow="hover">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-10px" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-20px text-12px text-gray-400 flex justify-between">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</ElCard>
</ElCol>
</ElRow>
</ElSkeleton>
</ElCard>
<ElCard shadow="never" class="mt-20px">
<template #header>
<div class="flex justify-between">
<span>{{ t('workplace.dynamic') }}</span>
<ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
</div>
</template>
<ElSkeleton :loading="loading" animated>
<div v-for="(item, index) in dynamics" :key="`dynamics-${index}`">
<div class="flex items-center">
<img
src="@/assets/imgs/avatar.jpg"
alt=""
class="w-35px h-35px rounded-[50%] mr-20px"
/>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ t('workplace.pushCode') }}
</Highlight>
</div>
<div class="mt-15px text-12px text-gray-400">
{{ useTimeAgo(item.time) }}
</div>
</div>
</div>
<ElDivider />
</div>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-20px">
<ElCard shadow="never">
<template #header>
<span>{{ t('workplace.shortcutOperation') }}</span>
</template>
<ElSkeleton :loading="loading" animated>
<ElRow>
<ElCol
v-for="item in 9"
:key="`card-${item}`"
:xl="12"
:lg="12"
:md="12"
:sm="24"
:xs="24"
class="mb-10px"
>
<ElLink type="default" :underline="false">
{{ t('workplace.operation') }}{{ item }}
</ElLink>
</ElCol>
</ElRow>
</ElSkeleton>
</ElCard>
<ElCard shadow="never" class="mt-20px">
<template #header>
<span>xx{{ t('workplace.index') }}</span>
</template>
<ElSkeleton :loading="loading" animated>
<Echart :options="radarOptionData" :height="400" />
</ElSkeleton>
</ElCard>
<ElCard shadow="never" class="mt-20px">
<template #header>
<span>{{ t('workplace.team') }}</span>
</template>
<ElSkeleton :loading="loading" animated>
<ElRow>
<ElCol v-for="item in team" :key="`team-${item.name}`" :span="12" class="mb-20px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-10px" />
<ElLink type="default" :underline="false">
{{ item.name }}
</ElLink>
</div>
</ElCol>
</ElRow>
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
</template>

View File

@@ -0,0 +1,318 @@
<script setup lang="ts">
import { ElRow, ElCol, ElCard, ElSkeleton } from 'element-plus'
import { CountTo } from '@/components/CountTo'
import { useDesign } from '@/hooks/web/useDesign'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive } from 'vue'
import { getRealTimeData } from '@/api/turnover'
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('panel')
const loading = ref(true)
let data: any = reactive({})
const getCount = async () => {
const res = await getRealTimeData()
.catch(() => {})
.finally(() => {
loading.value = false
})
data = Object.assign(data, res?.data || {})
}
getCount()
</script>
<template>
<ElRow :gutter="20" justify="space-between" :class="prefixCls">
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:peoples" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>下级推广数</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.anchorCount"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>总流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.totalTurnover"
:decimals="data.totalTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>上月流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.lastMonthTurnover"
:decimals="data.lastMonthTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>本月流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.currentMonthTurnover"
:decimals="data.currentMonthTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
<ElRow :gutter="20" justify="space-between" :class="prefixCls">
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>上周流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.lastWeekTurnover"
:decimals="data.lastWeekTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>本周流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.currentWeekTurnover"
:decimals="data.currentWeekTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>昨日流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.lastDayTurnover"
:decimals="data.lastDayTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="2">
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon icon="svg-icon:money" :size="40" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>今日流水</div
>
<CountTo
class="text-20px font-700 text-right"
:start-val="0"
:end-val="data.currentDayTurnover"
:decimals="data.currentDayTurnover ? 2 : 0"
:duration="2600"
/>
</div>
</div>
</template>
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{adminNamespace}-panel';
.@{prefix-cls} {
&__item {
&--peoples {
color: #40c9c6;
}
&--message {
color: #36a3f7;
}
&--money {
color: #f4516c;
}
&--shopping {
color: #34bfa3;
}
&:hover {
:deep(.@{adminNamespace}-icon) {
color: #fff !important;
}
.@{prefix-cls}__item--icon {
transition: all 0.38s ease-out;
}
.@{prefix-cls}__item--peoples {
background: #40c9c6;
}
.@{prefix-cls}__item--message {
background: #36a3f7;
}
.@{prefix-cls}__item--money {
background: #f4516c;
}
.@{prefix-cls}__item--shopping {
background: #34bfa3;
}
}
}
}
</style>

View File

@@ -0,0 +1,304 @@
import { EChartsOption } from 'echarts'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['日流水', '日邀请']
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
]
}
export const pieOptions: EChartsOption = {
title: {
text: t('analysis.userAccessSource'),
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: [
t('analysis.directAccess'),
t('analysis.mailMarketing'),
t('analysis.allianceAdvertising'),
t('analysis.videoAdvertising'),
t('analysis.searchEngines')
]
},
series: [
{
name: t('analysis.userAccessSource'),
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: t('analysis.directAccess') },
{ value: 310, name: t('analysis.mailMarketing') },
{ value: 234, name: t('analysis.allianceAdvertising') },
{ value: 135, name: t('analysis.videoAdvertising') },
{ value: 1548, name: t('analysis.searchEngines') }
]
}
]
}
export const barOptions: EChartsOption = {
title: {
text: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
type: 'bar'
}
]
}
export const radarOption: EChartsOption = {
legend: {
data: [t('workplace.personal'), t('workplace.team')]
},
radar: {
// shape: 'circle',
indicator: [
{ name: t('workplace.quote'), max: 65 },
{ name: t('workplace.contribution'), max: 160 },
{ name: t('workplace.hot'), max: 300 },
{ name: t('workplace.yield'), max: 130 },
{ name: t('workplace.follow'), max: 100 }
]
},
series: [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: [42, 30, 20, 35, 80],
name: t('workplace.personal')
},
{
value: [50, 140, 290, 100, 90],
name: t('workplace.team')
}
]
}
]
}
export const wordOptions = {
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: 600,
height: 400,
drawOutOfBound: true,
textStyle: {
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') +
')'
)
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: [
{
name: 'Sam S Club',
value: 10000,
textStyle: {
color: 'black'
},
emphasis: {
textStyle: {
color: 'red'
}
}
},
{
name: 'Macys',
value: 6181
},
{
name: 'Amy Schumer',
value: 4386
},
{
name: 'Jurassic World',
value: 4055
},
{
name: 'Charter Communications',
value: 2467
},
{
name: 'Chick Fil A',
value: 2244
},
{
name: 'Planet Fitness',
value: 1898
},
{
name: 'Pitch Perfect',
value: 1484
},
{
name: 'Express',
value: 1112
},
{
name: 'Home',
value: 965
},
{
name: 'Johnny Depp',
value: 847
},
{
name: 'Lena Dunham',
value: 582
},
{
name: 'Lewis Hamilton',
value: 555
},
{
name: 'KXAN',
value: 550
},
{
name: 'Mary Ellen Mark',
value: 462
},
{
name: 'Farrah Abraham',
value: 366
},
{
name: 'Rita Ora',
value: 360
},
{
name: 'Serena Williams',
value: 282
},
{
name: 'NCAA baseball tournament',
value: 273
},
{
name: 'Point Break',
value: 265
}
]
}
]
}

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const permissionStore = usePermissionStore()
const errorClick = () => {
push(permissionStore.addRouters[0]?.path as string)
}
</script>
<template>
<Error type="403" @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const permissionStore = usePermissionStore()
const errorClick = () => {
push(permissionStore.addRouters[0]?.path as string)
}
</script>
<template>
<Error @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const permissionStore = usePermissionStore()
const errorClick = () => {
push(permissionStore.addRouters[0]?.path as string)
}
</script>
<template>
<Error type="500" @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,340 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { Search } from '@/components/Search'
import { Dialog } from '@/components/Dialog'
import { useI18n } from '@/hooks/web/useI18n'
import { ElTag } from 'element-plus'
import { Table } from '@/components/Table'
import { getTableListApi, saveTableApi, delTableListApi } from '@/api/table'
import { useTable } from '@/hooks/web/useTable'
import { TableData } from '@/api/table/types'
import { ref, unref, reactive } from 'vue'
import Write from './components/Write.vue'
import Detail from './components/Detail.vue'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { BaseButton } from '@/components/Button'
const ids = ref<string[]>([])
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const { currentPage, pageSize } = tableState
const res = await getTableListApi({
pageIndex: unref(currentPage),
pageSize: unref(pageSize),
...unref(searchParams)
})
return {
list: res.data.list,
total: res.data.total
}
},
fetchDelApi: async () => {
const res = await delTableListApi(unref(ids))
return !!res
}
})
const { loading, dataList, total, currentPage, pageSize } = tableState
const { getList, getElTableExpose, delList } = tableMethods
const searchParams = ref({})
const setSearchParams = (params: any) => {
searchParams.value = params
getList()
}
const { t } = useI18n()
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
},
{
field: 'title',
label: t('tableDemo.title'),
search: {
component: 'Input'
},
form: {
component: 'Input',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'author',
label: t('tableDemo.author'),
search: {
hidden: true
}
},
{
field: 'display_time',
label: t('tableDemo.displayTime'),
search: {
hidden: true
},
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
},
{
field: 'importance',
label: t('tableDemo.importance'),
search: {
hidden: true
},
form: {
component: 'Select',
componentProps: {
style: {
width: '100%'
},
options: [
{
label: '重要',
value: 3
},
{
label: '良好',
value: 2
},
{
label: '一般',
value: 1
}
]
}
},
detail: {
slots: {
default: (data: any) => {
return (
<ElTag
type={
data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'
}
>
{data.importance === 1
? t('tableDemo.important')
: data.importance === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
}
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews'),
search: {
hidden: true
},
form: {
component: 'InputNumber',
value: 0
}
},
{
field: 'content',
label: t('exampleDemo.content'),
search: {
hidden: true
},
table: {
show: false
},
form: {
component: 'Editor',
colProps: {
span: 24
}
},
detail: {
span: 24,
slots: {
default: (data: any) => {
return <div innerHTML={data.content}></div>
}
}
}
},
{
field: 'action',
width: '260px',
label: t('tableDemo.action'),
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
slots: {
default: (data: any) => {
return (
<>
<BaseButton type="primary" onClick={() => action(data.row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(data.row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger" onClick={() => delData(data.row)}>
{t('exampleDemo.del')}
</BaseButton>
</>
)
}
}
}
}
])
// @ts-ignore
const { allSchemas } = useCrudSchemas(crudSchemas)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentRow = ref<TableData | null>(null)
const actionType = ref('')
const AddAction = () => {
dialogTitle.value = t('exampleDemo.add')
currentRow.value = null
dialogVisible.value = true
actionType.value = ''
}
const delLoading = ref(false)
const delData = async (row: TableData | null) => {
const elTableExpose = await getElTableExpose()
ids.value = row ? [row.id] : elTableExpose?.getSelectionRows().map((v: TableData) => v.id) || []
delLoading.value = true
await delList(unref(ids).length).finally(() => {
delLoading.value = false
})
}
const action = (row: TableData, type: string) => {
dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
actionType.value = type
currentRow.value = row
dialogVisible.value = true
}
const writeRef = ref<ComponentRef<typeof Write>>()
const saveLoading = ref(false)
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
saveLoading.value = true
const res = await saveTableApi(formData)
.catch(() => {})
.finally(() => {
saveLoading.value = false
})
if (res) {
dialogVisible.value = false
currentPage.value = 1
getList()
}
}
}
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
<BaseButton :loading="delLoading" type="danger" @click="delData(null)">
{{ t('exampleDemo.del') }}
</BaseButton>
</div>
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"
:columns="allSchemas.tableColumns"
:data="dataList"
:loading="loading"
:pagination="{
total: total
}"
@register="tableRegister"
/>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Write
v-if="actionType !== 'detail'"
ref="writeRef"
:form-schema="allSchemas.formSchema"
:current-row="currentRow"
/>
<Detail
v-if="actionType === 'detail'"
:detail-schema="allSchemas.detailSchema"
:current-row="currentRow"
/>
<template #footer>
<BaseButton
v-if="actionType !== 'detail'"
type="primary"
:loading="saveLoading"
@click="save"
>
{{ t('exampleDemo.save') }}
</BaseButton>
<BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { PropType } from 'vue'
import type { TableData } from '@/api/table/types'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
defineProps({
currentRow: {
type: Object as PropType<Nullable<TableData>>,
default: () => null
},
detailSchema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
}
})
</script>
<template>
<Descriptions :schema="detailSchema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch } from 'vue'
import { TableData } from '@/api/table/types'
import { useValidator } from '@/hooks/web/useValidator'
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<TableData>>,
default: () => null
},
formSchema: {
type: Array as PropType<FormSchema[]>,
default: () => []
}
})
const rules = reactive({
title: [required()],
author: [required()],
importance: [required()],
pageviews: [required()],
display_time: [required()],
content: [required()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose } = formMethods
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
return formData
}
}
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import Write from './components/Write.vue'
import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter } from 'vue-router'
import { saveTableApi } from '@/api/table'
import { useEventBus } from '@/hooks/event/useEventBus'
const { emit } = useEventBus()
const { push, go } = useRouter()
const { t } = useI18n()
const writeRef = ref<ComponentRef<typeof Write>>()
const loading = ref(false)
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
loading.value = true
const res = await saveTableApi(formData)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
emit('getList', 'add')
push('/example/example-page')
}
}
}
</script>
<template>
<ContentDetailWrap :title="t('exampleDemo.add')" @back="push('/example/example-page')">
<Write ref="writeRef" />
<template #header>
<BaseButton @click="go(-1)">
{{ t('common.back') }}
</BaseButton>
<BaseButton type="primary" :loading="loading" @click="save"
>{{ t('exampleDemo.save') }}
</BaseButton>
</template>
</ContentDetailWrap>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import Detail from './components/Detail.vue'
import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter, useRoute } from 'vue-router'
import { getTableDetApi } from '@/api/table'
import { TableData } from '@/api/table/types'
const { push, go } = useRouter()
const { query } = useRoute()
const { t } = useI18n()
const currentRow = ref<Nullable<TableData>>(null)
const getTableDet = async () => {
const res = await getTableDetApi(query.id as string)
if (res) {
currentRow.value = res.data
}
}
getTableDet()
</script>
<template>
<ContentDetailWrap :title="t('exampleDemo.detail')" @back="push('/example/example-page')">
<template #header>
<BaseButton @click="go(-1)">
{{ t('common.back') }}
</BaseButton>
</template>
<Detail :current-row="currentRow" />
</ContentDetailWrap>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import Write from './components/Write.vue'
import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter, useRoute } from 'vue-router'
import { saveTableApi, getTableDetApi } from '@/api/table'
import { TableData } from '@/api/table/types'
import { useEventBus } from '@/hooks/event/useEventBus'
const { emit } = useEventBus()
const { push, go } = useRouter()
const { query } = useRoute()
const { t } = useI18n()
const currentRow = ref<Nullable<TableData>>(null)
const getTableDet = async () => {
const res = await getTableDetApi(query.id as string)
if (res) {
currentRow.value = res.data
}
}
getTableDet()
const writeRef = ref<ComponentRef<typeof Write>>()
const loading = ref(false)
const save = async () => {
const write = unref(writeRef)
const formData = await write?.submit()
if (formData) {
loading.value = true
const res = await saveTableApi(formData)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
emit('getList', 'editor')
push('/example/example-page')
}
}
}
</script>
<template>
<ContentDetailWrap :title="t('exampleDemo.edit')" @back="push('/example/example-page')">
<Write ref="writeRef" :current-row="currentRow" />
<template #header>
<BaseButton @click="go(-1)">
{{ t('common.back') }}
</BaseButton>
<BaseButton type="primary" :loading="loading" @click="save">
{{ t('exampleDemo.save') }}
</BaseButton>
</template>
</ContentDetailWrap>
</template>
@/hooks/event/useEventBus

View File

@@ -0,0 +1,297 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { Search } from '@/components/Search'
import { useI18n } from '@/hooks/web/useI18n'
import { ElTag } from 'element-plus'
import { Table } from '@/components/Table'
import { getTableListApi, delTableListApi } from '@/api/table'
import { useTable } from '@/hooks/web/useTable'
import { TableData } from '@/api/table/types'
import { reactive, ref, unref } from 'vue'
import { useRouter } from 'vue-router'
import { useEventBus } from '@/hooks/event/useEventBus'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { BaseButton } from '@/components/Button'
defineOptions({
name: 'ExamplePage'
})
const { push } = useRouter()
const ids = ref<string[]>([])
const searchParams = ref({})
const setSearchParams = (params: any) => {
searchParams.value = params
getList()
}
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const { currentPage, pageSize } = tableState
const res = await getTableListApi({
pageIndex: unref(currentPage),
pageSize: unref(pageSize),
...unref(searchParams)
})
return {
list: res.data.list,
total: res.data.total
}
},
fetchDelApi: async () => {
const res = await delTableListApi(unref(ids))
return !!res
}
})
const { loading, dataList, total, currentPage, pageSize } = tableState
const { getList, getElTableExpose, delList } = tableMethods
getList()
useEventBus({
name: 'getList',
callback: (type: string) => {
if (type === 'add') {
currentPage.value = 1
}
getList()
}
})
const { t } = useI18n()
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
},
{
field: 'title',
label: t('tableDemo.title'),
search: {
component: 'Input'
},
form: {
component: 'Input',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'author',
label: t('tableDemo.author'),
search: {
hidden: true
}
},
{
field: 'display_time',
label: t('tableDemo.displayTime'),
search: {
hidden: true
},
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
},
{
field: 'importance',
label: t('tableDemo.importance'),
search: {
hidden: true
},
form: {
component: 'Select',
componentProps: {
style: {
width: '100%'
},
options: [
{
label: '重要',
value: 3
},
{
label: '良好',
value: 2
},
{
label: '一般',
value: 1
}
]
}
},
detail: {
slots: {
default: (data: any) => {
return (
<ElTag
type={
data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'
}
>
{data.importance === 1
? t('tableDemo.important')
: data.importance === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
}
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews'),
search: {
hidden: true
},
form: {
component: 'InputNumber',
value: 0
}
},
{
field: 'content',
label: t('exampleDemo.content'),
search: {
hidden: true
},
table: {
show: false
},
form: {
component: 'Editor',
colProps: {
span: 24
}
},
detail: {
span: 24,
slots: {
default: (data: any) => {
return <div innerHTML={data.content}></div>
}
}
}
},
{
field: 'action',
width: '260px',
label: t('tableDemo.action'),
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
slots: {
default: (data: any) => {
return (
<>
<BaseButton type="primary" onClick={() => action(data.row, 'edit')}>
{t('exampleDemo.edit')}
</BaseButton>
<BaseButton type="success" onClick={() => action(data.row, 'detail')}>
{t('exampleDemo.detail')}
</BaseButton>
<BaseButton type="danger" onClick={() => delData(data.row)}>
{t('exampleDemo.del')}
</BaseButton>
</>
)
}
}
}
}
])
// @ts-ignore
const { allSchemas } = useCrudSchemas(crudSchemas)
const AddAction = () => {
push('/example/example-add')
}
const delLoading = ref(false)
const delData = async (row: TableData | null) => {
const elTableExpose = await getElTableExpose()
ids.value = row ? [row.id] : elTableExpose?.getSelectionRows().map((v: TableData) => v.id) || []
delLoading.value = true
await delList(unref(ids).length).finally(() => {
delLoading.value = false
})
}
const action = (row: TableData, type: string) => {
push(`/example/example-${type}?id=${row.id}`)
}
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
<div class="mb-10px">
<BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
<BaseButton :loading="delLoading" type="danger" @click="delData(null)">
{{ t('exampleDemo.del') }}
</BaseButton>
</div>
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"
:columns="allSchemas.tableColumns"
:data="dataList"
:loading="loading"
:pagination="{
total: total
}"
@register="tableRegister"
/>
</ContentWrap>
</template>
@/hooks/event/useEventBus

View File

@@ -0,0 +1,69 @@
<script setup lang="tsx">
import { PropType, reactive } from 'vue'
import type { TableData } from '@/api/table/types'
import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
import { useI18n } from '@/hooks/web/useI18n'
import { ElTag } from 'element-plus'
const { t } = useI18n()
defineProps({
currentRow: {
type: Object as PropType<Nullable<TableData>>,
default: () => null
}
})
const schema = reactive<DescriptionsSchema[]>([
{
field: 'title',
label: t('exampleDemo.title'),
span: 24
},
{
field: 'author',
label: t('exampleDemo.author')
},
{
field: 'display_time',
label: t('exampleDemo.displayTime')
},
{
field: 'importance',
label: t('exampleDemo.importance'),
slots: {
default: (data: any) => {
return (
<ElTag
type={data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'}
>
{data.importance === 1
? t('tableDemo.important')
: data.importance === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
}
},
{
field: 'pageviews',
label: t('exampleDemo.pageviews')
},
{
field: 'content',
label: t('exampleDemo.content'),
span: 24,
slots: {
default: (data: any) => {
return <div innerHTML={data.content}></div>
}
}
}
])
</script>
<template>
<Descriptions :schema="schema" :data="currentRow || {}" />
</template>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch } from 'vue'
import { TableData } from '@/api/table/types'
import { useI18n } from '@/hooks/web/useI18n'
import { useValidator } from '@/hooks/web/useValidator'
import { IDomEditor } from '@wangeditor/editor'
const { required } = useValidator()
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<TableData>>,
default: () => null
}
})
const { t } = useI18n()
const { formRegister, formMethods } = useForm()
const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
const schema = reactive<FormSchema[]>([
{
field: 'title',
label: t('exampleDemo.title'),
component: 'Input',
formItemProps: {
rules: [required()]
},
colProps: {
span: 24
}
},
{
field: 'author',
label: t('exampleDemo.author'),
component: 'Input',
formItemProps: {
rules: [required()]
}
},
{
field: 'display_time',
label: t('exampleDemo.displayTime'),
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
},
formItemProps: {
rules: [required()]
}
},
{
field: 'importance',
label: t('exampleDemo.importance'),
component: 'Select',
formItemProps: {
rules: [required()]
},
componentProps: {
options: [
{
label: '重要',
value: 3
},
{
label: '良好',
value: 2
},
{
label: '一般',
value: 1
}
]
}
},
{
field: 'pageviews',
label: t('exampleDemo.pageviews'),
component: 'InputNumber',
value: 0,
formItemProps: {
rules: [required()]
}
},
{
field: 'content',
component: 'Editor',
colProps: {
span: 24
},
componentProps: {
defaultHtml: '',
// @ts-ignore
onChange: (edit: IDomEditor) => {
setValues({
content: edit.getHtml()
})
}
},
label: t('exampleDemo.content')
}
])
const rules = reactive({
title: [required()],
author: [required()],
importance: [required()],
pageviews: [required()],
display_time: [required()],
content: [required()]
})
const submit = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
const formData = await getFormData()
return formData
}
}
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
setValues(currentRow)
setSchema([
{
field: 'content',
path: 'componentProps.defaultHtml',
value: currentRow.content
}
])
},
{
deep: true,
immediate: true
}
)
defineExpose({
submit
})
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="schema" />
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const openTab = (item: number) => {
push(`/function/multiple-tabs-demo/${item}`)
}
</script>
<template>
<ContentWrap>
<BaseButton v-for="item in 5" :key="item" type="primary" @click="openTab(item)">
打开详情页{{ item }}
</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { ElInput } from 'element-plus'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useTagsView } from '@/hooks/web/useTagsView'
const { setTitle } = useTagsView()
const { params } = useRoute()
const val = ref(params.id as string)
setTitle(`详情页-${val.value}`)
</script>
<template>
<ContentWrap> 获取参数 <ElInput v-model="val" /> </ContentWrap>
</template>

View File

@@ -0,0 +1,173 @@
<script lang="ts" setup>
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { ElDivider } from 'element-plus'
import { request1, request2, request3, request4, request5, expired } from '@/api/request'
import { ref } from 'vue'
import request from '@/axios'
const { t } = useI18n()
const pending = ref<Set<string>>(new Set())
const getRequest1 = async () => {
if (pending.value.has('/request/1')) {
return
}
try {
pending.value.add('/request/1')
const res = await request1()
console.log('【res】', res)
} catch (error) {
console.log('【error】', error)
} finally {
pending.value.delete('/request/1')
}
}
const getRequest2 = async () => {
if (pending.value.has('/request/2')) {
return
}
try {
pending.value.add('/request/2')
const res = await request2()
console.log('【res】', res)
} catch (error) {
console.log('【error】', error)
} finally {
pending.value.delete('/request/2')
}
}
const getRequest3 = async () => {
if (pending.value.has('/request/3')) {
return
}
try {
pending.value.add('/request/3')
const res = await request3()
console.log('【res】', res)
} catch (error) {
console.log('【error】', error)
} finally {
pending.value.delete('/request/3')
}
}
const getRequest4 = async () => {
if (pending.value.has('/request/4')) {
return
}
try {
pending.value.add('/request/4')
const res = await request4()
console.log('【res】', res)
} catch (error) {
console.log('【error】', error)
} finally {
pending.value.delete('/request/4')
}
}
const getRequest5 = async () => {
if (pending.value.has('/request/5')) {
return
}
try {
pending.value.add('/request/5')
const res = await request5()
console.log('【res】', res)
} catch (error) {
console.log('【error】', error)
} finally {
pending.value.delete('/request/5')
}
}
const getAll = () => {
getRequest1()
getRequest2()
getRequest3()
getRequest4()
getRequest5()
}
const cancelAll = () => {
request.cancelAllRequest()
pending.value.clear()
}
// set转数组
const setToArray = (set: Set<string>) => {
const arr: string[] = []
set.forEach((item) => {
arr.push(item)
})
return arr
}
const clickRequest1 = () => {
if (pending.value.has('/request/1')) {
request.cancelRequest('/request/1')
pending.value.delete('/request/1')
return
}
getRequest1()
}
const clickRequest2 = () => {
if (pending.value.has('/request/2')) {
request.cancelRequest('/request/2')
pending.value.delete('/request/2')
return
}
getRequest2()
}
const clickRequest3 = () => {
if (pending.value.has('/request/3')) {
request.cancelRequest('/request/3')
pending.value.delete('/request/3')
return
}
getRequest3()
}
const clickRequest4 = () => {
if (pending.value.has('/request/4')) {
request.cancelRequest('/request/4')
pending.value.delete('/request/4')
return
}
getRequest4()
}
const clickRequest5 = () => {
if (pending.value.has('/request/5')) {
request.cancelRequest('/request/5')
pending.value.delete('/request/5')
return
}
getRequest5()
}
const tokenExpired = () => {
expired()
}
</script>
<template>
<ContentWrap :title="t('router.request')">
<p>正在请求的接口{{ setToArray(pending) }}</p>
<BaseButton type="primary" @click="clickRequest1">请求/取消request1</BaseButton>
<BaseButton type="primary" @click="clickRequest2">请求/取消request2</BaseButton>
<BaseButton type="primary" @click="clickRequest3">请求/取消request3</BaseButton>
<BaseButton type="primary" @click="clickRequest4">请求/取消request4</BaseButton>
<BaseButton type="primary" @click="clickRequest5">请求/取消request5</BaseButton>
<BaseButton type="primary" @click="getAll">发送五个请求</BaseButton>
<BaseButton type="primary" @click="cancelAll">关闭所有请求</BaseButton>
<ElDivider />
<BaseButton type="primary" @click="tokenExpired">token过期</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { ref, unref } from 'vue'
import { ElDivider, ElRow, ElCol } from 'element-plus'
import { hasPermi } from '@/components/Permission'
const permission = ref('add')
setTimeout(() => {
permission.value = 'view'
}, 3000)
</script>
<template>
<ContentWrap>
<ElDivider>组件方式判断已经全局注册支持动态修改</ElDivider>
<ElRow :gutter="20">
<ElCol :span="8">
新增权限
<Permission permission="add">
<BaseButton type="primary"> Add </BaseButton>
</Permission>
</ElCol>
<ElCol :span="8">
删除权限
<Permission permission="delete">
<BaseButton type="danger"> Delete </BaseButton>
</Permission>
</ElCol>
<ElCol :span="8">
3秒后切换查看权限
<Permission :permission="permission">
<BaseButton type="primary"> View </BaseButton>
</Permission>
</ElCol>
</ElRow>
<ElDivider>指令方式判断已经全局注册不支持动态修改</ElDivider>
<ElRow :gutter="20">
<ElCol :span="8">
新增权限
<BaseButton v-hasPermi="'add'" type="primary"> Add </BaseButton>
</ElCol>
<ElCol :span="8">
删除权限
<BaseButton v-hasPermi="'delete'" type="danger"> Delete </BaseButton>
</ElCol>
<ElCol :span="8">
3秒后切换查看权限无法动态修改
<BaseButton v-hasPermi="permission" type="primary"> View </BaseButton>
</ElCol>
</ElRow>
<ElDivider>函数方式判断</ElDivider>
<ElRow :gutter="20">
<ElCol :span="8">
新增权限
<BaseButton v-if="hasPermi('add')" type="primary"> Add </BaseButton>
</ElCol>
<ElCol :span="8">
删除权限
<BaseButton v-if="hasPermi('delete')" type="danger"> Delete </BaseButton>
</ElCol>
<ElCol :span="8">
3秒后切换查看权限
<BaseButton v-if="hasPermi(unref(permission))" type="primary"> View </BaseButton>
</ElCol>
</ElRow>
</ContentWrap>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { useGuide } from '@/hooks/web/useGuide'
const { t } = useI18n()
const { drive } = useGuide()
const guideStart = () => {
drive()
}
</script>
<template>
<ContentWrap :title="t('guideDemo.guide')" :message="t('guideDemo.message')">
<BaseButton type="primary" @click="guideStart">{{ t('guideDemo.start') }}</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ElInput } from 'element-plus'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { ref } from 'vue'
defineOptions({
name: 'Menu111'
})
const { t } = useI18n()
const text = ref('')
</script>
<template>
<ContentWrap :title="t('levelDemo.menu')">
<div class="flex items-center"> Menu111: <ElInput v-model="text" class="pl-20px" /> </div>
</ContentWrap>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ElInput } from 'element-plus'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { ref } from 'vue'
defineOptions({
name: 'Menu12'
})
const { t } = useI18n()
const text = ref('')
</script>
<template>
<ContentWrap :title="t('levelDemo.menu')">
<div class="flex items-center"> Menu12: <ElInput v-model="text" class="pl-20px" /> </div>
</ContentWrap>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ElInput } from 'element-plus'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { ref } from 'vue'
defineOptions({
name: 'Menu2'
})
const { t } = useI18n()
const text = ref('')
</script>
<template>
<ContentWrap :title="t('levelDemo.menu')">
<div class="flex items-center"> Menu2: <ElInput v-model="text" class="pl-20px" /> </div>
</ContentWrap>
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { LoginForm, RegisterForm } from './components'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { LocaleDropdown } from '@/components/LocaleDropdown'
import { useI18n } from '@/hooks/web/useI18n'
import { underlineToHump } from '@/utils'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { ref } from 'vue'
import { ElScrollbar } from 'element-plus'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('login')
const appStore = useAppStore()
const { t } = useI18n()
const isLogin = ref(true)
const toRegister = () => {
isLogin.value = false
}
const toLogin = () => {
isLogin.value = true
}
</script>
<template>
<div
:class="prefixCls"
class="h-[100%] relative lt-xl:bg-[var(--login-bg-color)] lt-sm:px-10px lt-xl:px-10px lt-md:px-10px"
>
<ElScrollbar class="h-full">
<div class="relative flex mx-auto min-h-100vh">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
>
<div class="flex items-center relative text-white">
<img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<div class="flex justify-center items-center h-[calc(100%-60px)]">
<TransitionGroup
appear
tag="div"
enter-active-class="animate__animated animate__bounceInLeft"
>
<img src="@/assets/svgs/login-box-bg.svg" key="1" alt="" class="w-350px" />
<div class="text-3xl text-white" key="2">{{ t('login.welcome') }}</div>
<div class="mt-5 font-normal text-white text-14px" key="3">
{{ t('login.message') }}
</div>
</TransitionGroup>
</div>
</div>
<div class="flex-1 p-30px lt-sm:p-10px dark:bg-[var(--login-bg-color)] relative">
<div
class="flex justify-between items-center text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center at-2xl:hidden at-xl:hidden">
<img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<div class="flex justify-end items-center space-x-10px">
<ThemeSwitch />
<LocaleDropdown class="lt-xl:text-white dark:text-white" />
</div>
</div>
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="h-full flex items-center m-auto w-[100%] at-2xl:max-w-500px at-xl:max-w-500px at-md:max-w-500px at-lg:max-w-500px"
>
<LoginForm
v-if="isLogin"
class="p-20px h-auto m-auto lt-xl:rounded-3xl lt-xl:light:bg-white"
@to-register="toRegister"
/>
<RegisterForm
v-else
class="p-20px h-auto m-auto lt-xl:rounded-3xl lt-xl:light:bg-white"
@to-login="toLogin"
/>
</div>
</Transition>
</div>
</div>
</ElScrollbar>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{adminNamespace}-login';
.@{prefix-cls} {
overflow: auto;
&__left {
&::before {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-image: url('@/assets/svgs/login-bg.svg');
background-position: center;
background-repeat: no-repeat;
content: '';
}
}
}
</style>

View File

@@ -0,0 +1,300 @@
<script setup lang="tsx">
import { getAdminRoleApi, loginApi } from '@/api/login'
import { UserType } from '@/api/login/types'
import { getInfo, getShareDomain } from '@/api/user'
import { BaseButton } from '@/components/Button'
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { useI18n } from '@/hooks/web/useI18n'
import { useValidator } from '@/hooks/web/useValidator'
import { role01, role04 } from '@/router/role'
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import { useUserStore } from '@/store/modules/user'
import { ElCheckbox } from 'element-plus'
import { onMounted, reactive, ref, unref, watch } from 'vue'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { useRouter } from 'vue-router'
const { required } = useValidator()
const emit = defineEmits(['to-register'])
const appStore = useAppStore()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const { currentRoute, addRoute, push } = useRouter()
const { t } = useI18n()
const rules = {
username: [required()],
password: [required()]
}
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return <h2 class="text-2xl font-bold text-center w-[100%]">{t('login.login')}</h2>
}
}
}
},
{
field: 'username',
label: t('login.username'),
// value: 'admin',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: '请输入账号'
}
},
{
field: 'password',
label: t('login.password'),
// value: 'admin',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
placeholder: '请输入密码',
// 按下enter键触发登录
onKeydown: (_e: any) => {
if (_e.key === 'Enter') {
_e.stopPropagation() // 阻止事件冒泡
signIn()
}
}
}
},
{
field: 'tool',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="flex justify-between items-center w-[100%]">
<ElCheckbox v-model={remember.value} label={t('login.remember')} size="small" />
{/* <ElLink type="primary" underline={false}>
{t('login.forgetPassword')}
</ElLink> */}
</div>
</>
)
}
}
}
},
{
field: 'login',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="w-[100%]">
<BaseButton
loading={loading.value}
type="primary"
class="w-[100%]"
onClick={signIn}
>
{t('login.login')}
</BaseButton>
</div>
<div class="w-[100%] mt-15px">
<BaseButton class="w-[100%]" onClick={toRegister}>
{t('login.register')}
</BaseButton>
</div>
</>
)
}
}
}
}
])
const remember = ref(userStore.getRememberMe)
const initLoginInfo = () => {
const loginInfo = userStore.getLoginInfo
if (loginInfo) {
const { username, password } = loginInfo
setValues({ username, password })
}
}
onMounted(() => {
initLoginInfo()
})
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose, setValues } = formMethods
const loading = ref(false)
const redirect = ref<string>('')
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// 登录
const signIn = async () => {
const formRef = await getElFormExpose()
await formRef?.validate(async (isValid) => {
if (isValid) {
loading.value = true
const formData = await getFormData<UserType>()
try {
const res = (await loginApi(formData)) as any
if (res) {
// 是否记住我
if (unref(remember)) {
userStore.setLoginInfo({
username: formData.username,
password: formData.password
})
} else {
userStore.setLoginInfo(undefined)
}
userStore.setToken(res.token)
userStore.setRememberMe(unref(remember))
const res2 = await getInfo()
const shareDomainRes = await getShareDomain()
let shareLink = `${shareDomainRes.data.shareDomain}`.replace("${code}", res2.data.invitationCode);
if (res2.data.userType === '01') {
shareLink = `${shareDomainRes.data.zhuboShareDomain}`.replace("${code}", res2.data.invitationCode);
} else {
shareLink = `${shareDomainRes.data.zhaoshaoShareDomain}`.replace("${code}", res2.data.invitationCode);
}
userStore.setUserInfo({
uid: res2.data.userId,
username: formData.username,
password: formData.password,
role: res2.data.userType,
roleId: '1',
token: res.token,
shareLink: shareLink,
shareDomain: res2.data.shareDomain
})
// 是否使用动态路由
if (appStore.getDynamicRouter) {
getRole(res2.data.userType)
} else {
await permissionStore.generateRoutes('static').catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
})
permissionStore.setIsAddRouters(true)
getInfo().then((res2) => {
userStore.setUserInfo({
uid: res2.data.userId,
username: formData.username,
password: formData.password,
role: res2.data.userType,
roleId: '1',
token: res.token,
shareLink: shareLink,
shareDomain: res2.data.shareDomain
})
push({ path: redirect.value || permissionStore.addRouters[0].path })
})
}
}
} finally {
loading.value = false
}
}
})
}
const getRoleRouters = async (roleType: string) => {
if (roleType === '01') {
return {
code: 0,
data: role01
}
}
return {
code: 0,
data: role04
}
}
// 获取角色信息
const getRole = async (roleType: string) => {
const formData = await getFormData<UserType>()
const params = {
roleName: formData.username
}
const res =
appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await getAdminRoleApi(params)
: await getRoleRouters(roleType)
if (res) {
const routers = res.data || []
userStore.setRoleRouters(routers)
appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await permissionStore.generateRoutes('server', routers).catch(() => {})
: await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
}
// 去注册页面
const toRegister = () => {
emit('to-register')
}
</script>
<template>
<Form
:schema="schema"
:rules="rules"
label-position="top"
hide-required-asterisk
size="large"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
@register="formRegister"
/>
</template>

View File

@@ -0,0 +1,251 @@
<script setup lang="tsx">
import { Form, FormSchema } from '@/components/Form'
import { reactive, ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useForm } from '@/hooks/web/useForm'
import { ElInput, ElMessage, FormRules } from 'element-plus'
import { useValidator } from '@/hooks/web/useValidator'
import { BaseButton } from '@/components/Button'
import { IAgree } from '@/components/IAgree'
import { RegisterUserType } from '@/api/login/types'
import { registerApi, getCodeApi } from '@/api/login'
const emit = defineEmits(['to-login'])
const { formRegister, formMethods } = useForm()
const { getElFormExpose, getFormData } = formMethods
const { t } = useI18n()
const { required, check } = useValidator()
const getCodeTime = ref(60)
const getCodeLoading = ref(false)
const getCode = async () => {
const formData = await getFormData<RegisterUserType>()
if (!formData.username) {
ElMessage.error('手机号不能为空')
} else {
getCodeApi(formData.username)
.then(() => {
ElMessage.success('获取验证码成功')
getCodeLoading.value = true
const timer = setInterval(() => {
getCodeTime.value--
if (getCodeTime.value <= 0) {
clearInterval(timer)
getCodeTime.value = 60
getCodeLoading.value = false
}
}, 1000)
})
.catch(() => {
ElMessage.error('获取验证码失败')
})
}
}
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return <h2 class="text-2xl font-bold text-center w-[100%]">{t('login.register')}</h2>
}
}
}
},
{
field: 'username',
label: t('login.username'),
value: '',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: '请输入手机号'
}
},
{
field: 'password',
label: t('login.password'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
}
},
{
field: 'check_password',
label: t('login.checkPassword'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
}
},
{
field: 'parentInvitationCode',
label: '上级邀请码',
value: '',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: '六位数邀请码'
}
},
{
field: 'code',
label: t('login.code'),
colProps: {
span: 24
},
formItemProps: {
slots: {
default: (formData) => {
return (
<div class="w-[100%] flex">
<ElInput v-model={formData.code} placeholder={t('login.codePlaceholder')} />
<BaseButton
type="primary"
disabled={unref(getCodeLoading)}
class="ml-10px"
onClick={getCode}
>
{t('login.getCode')}
{unref(getCodeLoading) ? `(${unref(getCodeTime)})` : ''}
</BaseButton>
</div>
)
}
}
}
},
// {
// field: 'iAgree',
// colProps: {
// span: 24
// },
// formItemProps: {
// slots: {
// default: (formData: any) => {
// return (
// <>
// <IAgree
// v-model={formData.iAgree}
// text="我同意《用户协议》"
// link={[
// {
// text: '《用户协议》',
// url: 'https://element-plus.org/'
// }
// ]}
// />
// </>
// )
// }
// }
// }
// },
{
field: 'register',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="w-[100%]">
<BaseButton
type="primary"
class="w-[100%]"
loading={loading.value}
onClick={loginRegister}
>
{t('login.register')}
</BaseButton>
</div>
<div class="w-[100%] mt-15px">
<BaseButton class="w-[100%]" onClick={toLogin}>
{t('login.hasUser')}
</BaseButton>
</div>
</>
)
}
}
}
}
])
const rules: FormRules = {
username: [required()],
password: [required()],
check_password: [required()],
code: [required()],
iAgree: [required(), check()]
}
const toLogin = () => {
emit('to-login')
}
const loading = ref(false)
const loginRegister = async () => {
const formRef = await getElFormExpose()
formRef?.validate(async (valid) => {
if (valid) {
try {
loading.value = true
const formData = await getFormData<RegisterUserType>()
if (formData.password !== formData.check_password) {
ElMessage.error('两次输入的密码不一致')
}
const res = await registerApi(formData)
if (res) {
toLogin()
}
} finally {
loading.value = false
}
}
})
}
</script>
<template>
<Form
:schema="schema"
:rules="rules"
label-position="top"
hide-required-asterisk
size="large"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
@register="formRegister"
/>
</template>

View File

@@ -0,0 +1,4 @@
import LoginForm from './LoginForm.vue'
import RegisterForm from './RegisterForm.vue'
export { LoginForm, RegisterForm }

View File

@@ -0,0 +1,105 @@
<template>
<div class="app-container">
<el-row>
<el-col :span="12">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12">
<div class="grid-content ep-bg-purple-light" style="text-align: right">
<el-text class="mx-1">我的历史总奖励:</el-text
><el-text class="mx-1" type="success" size="large">{{ totalReward }}</el-text>
</div>
</el-col>
</el-row>
<el-table :data="tableData">
<el-table-column label="奖励金额" prop="reward" />
<el-table-column label="日期" prop="date" />
</el-table>
<el-pagination
layout="prev, pager, next"
:page-size="10"
:total="total"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { queryPromoteWelfare } from '@/api/turnover'
import {
ElDatePicker,
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElButton,
ElPagination,
ElRow,
ElCol,
ElText
} from 'element-plus'
// 定义查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
beginTime: undefined as string | undefined,
endTime: undefined as string | undefined
})
// 表格数据
const tableData = ref([])
// 日期范围
const dateRange = ref([])
const total = ref(0)
const totalReward = ref(0)
/** 获取表格数据 */
const getTableData = () => {
queryPromoteWelfare(queryParams).then((response) => {
tableData.value = (response as any).data.rows
total.value = response.data.total
totalReward.value = response.data.totalReward
})
}
/** 搜索按钮操作 */
const handleQuery = () => {
// queryParams.pageNum = 1 // 如果需要重置页码,取消注释此行
if (dateRange.value !== null && dateRange.value.length === 2) {
queryParams.beginTime = dateRange.value[0]
queryParams.endTime = dateRange.value[1]
} else {
queryParams.beginTime = undefined
queryParams.endTime = undefined
}
getTableData()
}
const handleCurrentChange = (val: number) => {
queryParams.pageNum = val
handleQuery()
}
// 组件挂载时获取数据
onMounted(() => {
getTableData()
})
</script>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { ref, unref } from 'vue'
import { ElDivider, ElImage, ElTag, ElTabPane, ElTabs, ElButton, ElMessage } from 'element-plus'
import defaultAvatar from '@/assets/imgs/avatar.jpg'
import UploadAvatar from './components/UploadAvatar.vue'
import { Dialog } from '@/components/Dialog'
import EditInfo from './components/EditInfo.vue'
import EditPassword from './components/EditPassword.vue'
const userInfo = ref()
const fetchDetailUserApi = async () => {
// 这里可以调用接口获取用户信息
const data = {
id: 1,
username: 'admin',
realName: 'admin',
phoneNumber: '18888888888',
email: '502431556@qq.com',
avatarUrl: '',
roleList: ['超级管理员']
}
userInfo.value = data
}
fetchDetailUserApi()
const activeName = ref('first')
const dialogVisible = ref(false)
const uploadAvatarRef = ref<ComponentRef<typeof UploadAvatar>>()
const avatarLoading = ref(false)
const saveAvatar = async () => {
try {
avatarLoading.value = true
const base64 = unref(uploadAvatarRef)?.getBase64()
console.log(base64)
// 这里可以调用修改头像接口
fetchDetailUserApi()
ElMessage.success('修改成功')
dialogVisible.value = false
} catch (error) {
console.log(error)
} finally {
avatarLoading.value = false
}
}
</script>
<template>
<div class="flex w-100% h-100%">
<ContentWrap title="个人信息" class="w-400px">
<div class="flex justify-center items-center">
<div
class="avatar w-[150px] h-[150px] relative cursor-pointer"
@click="dialogVisible = true"
>
<ElImage
class="w-[150px] h-[150px] rounded-full"
:src="userInfo?.avatarUrl || defaultAvatar"
fit="fill"
/>
</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>账号</div>
<div>{{ userInfo?.username }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>昵称</div>
<div>{{ userInfo?.realName }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>手机号码</div>
<div>{{ userInfo?.phoneNumber ?? '-' }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>用户邮箱</div>
<div>{{ userInfo?.email ?? '-' }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>所属角色</div>
<div>
<template v-if="userInfo?.roleList?.length">
<ElTag v-for="item in userInfo?.roleList || []" :key="item" class="ml-2 mb-w"
>{{ item }}
</ElTag>
</template>
<template v-else>-</template>
</div>
</div>
<ElDivider />
</ContentWrap>
<ContentWrap title="基本资料" class="flex-[3] ml-20px">
<ElTabs v-model="activeName">
<ElTabPane label="基本信息" name="first">
<EditInfo :user-info="userInfo" />
</ElTabPane>
<ElTabPane label="修改密码" name="second">
<EditPassword />
</ElTabPane>
</ElTabs>
</ContentWrap>
</div>
<Dialog v-model="dialogVisible" title="修改头像" width="800px">
<UploadAvatar ref="uploadAvatarRef" :url="userInfo?.avatarUrl || defaultAvatar" />
<template #footer>
<ElButton type="primary" :loading="avatarLoading" @click="saveAvatar"> 保存 </ElButton>
<ElButton @click="dialogVisible = false">关闭</ElButton>
</template>
</Dialog>
</template>
<style lang="less" scoped>
.avatar {
position: relative;
&::after {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
font-size: 50px;
color: #fff;
background-color: rgb(0 0 0 / 40%);
border-radius: 50%;
content: '+';
opacity: 0;
justify-content: center;
align-items: center;
}
&:hover {
&::after {
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<script lang="ts" setup>
import { FormSchema, Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { useValidator } from '@/hooks/web/useValidator'
import { reactive, ref, watch } from 'vue'
import { ElDivider, ElMessage, ElMessageBox } from 'element-plus'
const props = defineProps({
userInfo: {
type: Object,
default: () => ({})
}
})
const { required, phone, maxlength, email } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'realName',
label: '昵称',
component: 'Input',
colProps: {
span: 24
}
},
{
field: 'phoneNumber',
label: '手机号码',
component: 'Input',
colProps: {
span: 24
}
},
{
field: 'email',
label: '邮箱',
component: 'Input',
colProps: {
span: 24
}
}
])
const rules = reactive({
realName: [required(), maxlength(50)],
phoneNumber: [phone()],
email: [email()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getElFormExpose } = formMethods
watch(
() => props.userInfo,
(value) => {
setValues(value)
},
{
immediate: true,
deep: true
}
)
const saveLoading = ref(false)
const save = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
ElMessageBox.confirm('是否确认修改?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
saveLoading.value = true
// 这里可以调用修改用户信息接口
ElMessage.success('修改成功')
} catch (error) {
console.log(error)
} finally {
saveLoading.value = false
}
})
.catch(() => {})
}
}
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
<ElDivider />
<BaseButton type="primary" @click="save">保存</BaseButton>
</template>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { ElMessage, ElMessageBox, ElDivider } from 'element-plus'
const { required } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'password',
label: '旧密码',
component: 'InputPassword',
colProps: {
span: 24
}
},
{
field: 'newPassword',
label: '新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
strength: true
}
},
{
field: 'newPassword2',
label: '确认新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
strength: true
}
}
])
const rules = reactive({
password: [required()],
newPassword: [
required(),
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { newPassword2 } = formData
if (val !== newPassword2) {
callback(new Error('新密码与确认新密码不一致'))
} else {
callback()
}
}
}
],
newPassword2: [
required(),
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { newPassword } = formData
if (val !== newPassword) {
callback(new Error('确认新密码与新密码不一致'))
} else {
callback()
}
}
}
]
})
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose } = formMethods
const saveLoading = ref(false)
const save = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
ElMessageBox.confirm('是否确认修改?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
saveLoading.value = true
// 这里可以调用修改密码的接口
ElMessage.success('修改成功')
} catch (error) {
console.log(error)
} finally {
saveLoading.value = false
}
})
.catch(() => {})
}
}
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
<ElDivider />
<BaseButton type="primary" @click="save">确认修改</BaseButton>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { ImageCropping } from '@/components/ImageCropping'
import { ref, unref } from 'vue'
defineProps({
url: {
type: String,
default: ''
}
})
const fileUrl = ref('')
const CropperRef = ref<ComponentRef<typeof ImageCropping>>()
const getBase64 = () => {
const base64 = unref(CropperRef)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
return base64
}
defineExpose({
getBase64
})
</script>
<template>
<div>
<ImageCropping ref="CropperRef" :image-url="fileUrl || url" />
</div>
</template>

View File

@@ -0,0 +1,30 @@
<template>
<div></div>
</template>
<script setup lang="ts">
import { unref } from 'vue'
import { useRouter } from 'vue-router'
const { currentRoute, replace } = useRouter()
const { params, query } = unref(currentRoute)
const { path, _redirect_type = 'path' } = params
Reflect.deleteProperty(params, '_redirect_type')
Reflect.deleteProperty(params, 'path')
const _path = Array.isArray(path) ? path.join('/') : path
if (_redirect_type === 'name') {
replace({
name: _path,
query,
params
})
} else {
replace({
path: _path.startsWith('/') ? _path : '/' + _path,
query
})
}
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="主播ID">
<el-input v-model="queryParams.parentId" clearable />
</el-form-item>
<el-form-item label="主播昵称">
<el-input v-model="queryParams.parentNickName" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData">
<el-table-column label="主播ID" prop="userId" />
<el-table-column label="主播昵称" prop="nickName" />
<!-- <el-table-column label="日消费金额" prop="dailyTotal" /> -->
<el-table-column label="主播下级总消费" prop="totalByParent" />
<!-- <el-table-column label="日期" prop="date" /> -->
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getAnchorPromotionData } from '@/api/turnover'
import {
ElDatePicker,
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElInput,
ElButton
} from 'element-plus'
// 定义查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
beginTime: undefined as string | undefined,
endTime: undefined as string | undefined,
parentId: undefined as number | undefined,
anchorName: undefined as string | undefined,
parentNickName: undefined as string | undefined
})
// 表格数据
const tableData = ref([])
// 日期范围
const dateRange = ref([])
/** 获取表格数据 */
const getTableData = () => {
getAnchorPromotionData(queryParams).then((response) => {
tableData.value = (response as any).rows
})
}
/** 搜索按钮操作 */
const handleQuery = () => {
// queryParams.pageNum = 1 // 如果需要重置页码,取消注释此行
if (dateRange.value !== null && dateRange.value.length === 2) {
queryParams.beginTime = dateRange.value[0]
queryParams.endTime = dateRange.value[1]
} else {
queryParams.beginTime = undefined
queryParams.endTime = undefined
}
getTableData()
}
// 组件挂载时获取数据
onMounted(() => {
getTableData()
})
</script>

View File

@@ -0,0 +1,162 @@
<template>
<div class="app-container">
<el-descriptions title="用户信息" :column="1" size="default" border>
<el-descriptions-item label="手机号">{{ form.userName }}</el-descriptions-item>
<el-descriptions-item label="推广码"
>{{ form.invitationCode }}
<el-button type="primary" @click="editInvitationCode">编辑</el-button></el-descriptions-item
>
<el-descriptions-item label="邀请链接">{{ shareLink }}</el-descriptions-item>
<el-descriptions-item label="余额">{{ form.accountAmount }}</el-descriptions-item>
<el-descriptions-item label="积分">{{ form.accountCredits }}</el-descriptions-item>
</el-descriptions>
<el-button type="primary" @click="rechargeDialogVisible = true">提取余额</el-button>
<el-dialog v-model="dialogVisible" title="编辑推广码" width="500">
<el-form :model="form" label-width="auto" style="max-width: 600px">
<el-form-item label="推广码">
<el-input v-model="form.invitationCode" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"> 确认 </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="rechargeDialogVisible" title="生成卡密" width="500">
<el-form ref="form2" :model="generateForm" label-width="100px">
<el-form-item label="生成数量" prop="num">
<el-input-number v-model="generateForm.num" :min="0" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="金额(元)" prop="rechargeListId">
<el-select
v-model="generateForm.rechargeListId"
filterable
placeholder="请选择充值列表ID"
clearable
>
<el-option
v-for="item in rechangeList"
:key="item.id"
:label="item.price"
:value="item.id"
>
{{ item.price }}
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="rechargeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="onGenerateCard()"> 确认 </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="cardDialogVisible" title="卡密结果" width="500">
<div v-for="(item, index) in overList" :key="index" style="margin-top: 1px">{{ item }}</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cardDialogVisible = false">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import {
ElInputNumber,
ElSelect,
ElOption,
ElDescriptions,
ElDescriptionsItem,
ElForm,
ElFormItem,
ElInput,
ElButton,
ElMessage,
ElDialog,
} from 'element-plus'
import {
getInfo,
updateInvitationCode,
getShareDomain,
getRechargeListApi,
generateCardListApi
} from '@/api/user'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
// 定义响应式状态
const form = reactive({} as any)
const shareLink = ref('')
const dialogVisible = ref(false)
const rechargeDialogVisible = ref(false)
const generateForm = reactive({
num: 0,
rechargeListId: 0
})
const rechangeList = reactive([] as any)
const overList = reactive([] as any)
const cardDialogVisible = ref(false)
const freshUserInfo = async () => {
const res = await getInfo()
Object.assign(form, res.data)
shareLink.value = await getShareLink()
console.log('用户信息已刷新', form)
}
const onSubmit = async () => {
rechargeDialogVisible.value = false
// 这里可以添加表单提交逻辑,例如调用 API 更新用户信息
await updateInvitationCode({
username: form.userName,
invitationCode: form.invitationCode
})
ElMessage.success('信息已更新')
freshUserInfo() // 刷新用户信息
}
const editInvitationCode = () => {
dialogVisible.value = true
}
const getShareLink = async () => {
const shareDomainRes = await getShareDomain()
let shareLink = `${shareDomainRes.data.shareDomain}`.replace('${code}', form.invitationCode)
if (form.userType === '01') {
shareLink = `${shareDomainRes.data.zhuboShareDomain}`.replace('${code}', form.invitationCode)
userStore.userInfo!.shareDomain = shareDomainRes.data.zhuboShareDomain
} else {
shareLink = `${shareDomainRes.data.zhaoshaoShareDomain}`.replace('${code}', form.invitationCode)
userStore.userInfo!.shareDomain = shareDomainRes.data.zhaoshaoShareDomain
}
userStore.userInfo!.shareLink = shareLink
return shareLink
}
const onGenerateCard = async () => {
rechargeDialogVisible.value = false
cardDialogVisible.value = true
const res = await generateCardListApi(generateForm.rechargeListId, generateForm.num)
Object.assign(overList, res.data)
freshUserInfo()
}
const getRechargeList = async () => {
const res = await getRechargeListApi()
Object.assign(rechangeList, res.rows)
}
// 组件挂载时初始化
onMounted(() => {
freshUserInfo()
getRechargeList()
})
</script>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useClipboard } from '@/hooks/web/useClipboard'
import { ElInput } from 'element-plus'
import { ref } from 'vue'
const { copy, copied, text, isSupported } = useClipboard()
const source = ref('')
</script>
<template>
<ContentWrap title="useClipboard">
<ElInput v-model="source" placeholder="请输入要复制的内容" />
<div v-if="isSupported">
<BaseButton @click="copy(source)" type="primary" class="mt-20px">
<span v-if="!copied">复制</span>
<span v-else>已复制</span>
</BaseButton>
<p>
当前已复制: <code>{{ text || 'none' }}</code>
</p>
</div>
<p v-else> 你的浏览器不支持 Clipboard API </p>
</ContentWrap>
</template>

View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { useI18n } from '@/hooks/web/useI18n'
import { reactive } from 'vue'
import { JsonEditor } from '@/components/JsonEditor'
import { ContentWrap } from '@/components/ContentWrap'
import { ElRow, ElCol } from 'element-plus'
const { t } = useI18n()
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
},
{
field: 'title',
label: t('tableDemo.title'),
search: {
component: 'Input'
},
form: {
component: 'Input',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'author',
label: t('tableDemo.author'),
search: {
hidden: true
}
},
{
field: 'display_time',
label: t('tableDemo.displayTime'),
search: {
hidden: true
},
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
},
{
field: 'importance',
label: t('tableDemo.importance'),
search: {
hidden: true
},
form: {
component: 'Select',
componentProps: {
style: {
width: '100%'
},
options: [
{
label: '重要',
value: 3
},
{
label: '良好',
value: 2
},
{
label: '一般',
value: 1
}
]
}
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews'),
search: {
hidden: true
},
form: {
component: 'InputNumber',
value: 0
}
},
{
field: 'content',
label: t('exampleDemo.content'),
search: {
hidden: true
},
table: {
show: false
},
form: {
component: 'Editor',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'action',
width: '260px',
label: t('tableDemo.action'),
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
}
])
const { allSchemas } = useCrudSchemas(crudSchemas)
</script>
<template>
<ContentWrap title="useCrudSchemas">
<ElRow :gutter="20">
<ElCol :span="24">
<ContentWrap title="原始数据数据" class="mt-20px">
<JsonEditor v-model="crudSchemas" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="查询组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.searchSchema" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="表单组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.formSchema" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="表格组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.tableColumns" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="详情组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.detailSchema" />
</ContentWrap>
</ElCol>
</ElRow>
</ContentWrap>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useNetwork } from '@/hooks/web/useNetwork'
const { online } = useNetwork()
</script>
<template>
<ContentWrap title="useNetwork">
当前网络状态: <code>{{ online ? '已连接' : '已断开' }}</code>
</ContentWrap>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useTagsView } from '@/hooks/web/useTagsView'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } =
useTagsView()
const closeAllTabs = () => {
closeAll(() => {
push('/dashboard/analysis')
})
}
const closeLeftTabs = () => {
closeLeft()
}
const closeRightTabs = () => {
closeRight()
}
const closeOtherTabs = () => {
closeOther()
}
const refresh = () => {
refreshPage()
}
const closeCurrentTab = () => {
closeCurrent(undefined, () => {
push('/dashboard/analysis')
})
}
const setTabTitle = () => {
setTitle(new Date().getTime().toString())
}
const setAnalysisTitle = () => {
setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/analysis')
}
</script>
<template>
<ContentWrap title="useTagsView">
<BaseButton type="primary" @click="closeAllTabs"> 关闭所有标签页 </BaseButton>
<BaseButton type="primary" @click="closeLeftTabs"> 关闭左侧标签页 </BaseButton>
<BaseButton type="primary" @click="closeRightTabs"> 关闭右侧标签页 </BaseButton>
<BaseButton type="primary" @click="closeOtherTabs"> 关闭其他标签页 </BaseButton>
<BaseButton type="primary" @click="closeCurrentTab"> 关闭当前标签页 </BaseButton>
<BaseButton type="primary" @click="refresh"> 刷新当前标签页 </BaseButton>
<BaseButton type="primary" @click="setTabTitle"> 修改当前标题 </BaseButton>
<BaseButton type="primary" @click="setAnalysisTitle"> 修改分析页标题 </BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { Form, FormSchema } from '@/components/Form'
import { useValidator } from '@/hooks/web/useValidator'
import { useForm } from '@/hooks/web/useForm'
import { reactive } from 'vue'
import { FormItemRule } from 'element-plus'
const { formRegister, formMethods } = useForm()
const { getFormData } = formMethods
const { required, lengthRange, notSpace, notSpecialCharacters } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'field1',
label: '必填',
component: 'Input'
},
{
field: 'field2',
label: '长度范围',
component: 'Input'
},
{
field: 'field3',
label: '不能有空格',
component: 'Input'
},
{
field: 'field4',
label: '不能有特殊字符',
component: 'Input'
},
{
field: 'field5',
label: '是否相等-值1',
component: 'Input'
},
{
field: 'field6',
label: '是否相等-值2',
component: 'Input'
}
])
const rules = reactive<{
[key: string]: FormItemRule[]
}>({
field1: [required()],
field2: [
lengthRange({
min: 2,
max: 5
})
],
field3: [notSpace()],
field4: [notSpecialCharacters()],
field5: [
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { field6 } = formData
if (val !== field6) {
callback(new Error('两个值不相等'))
} else {
callback()
}
}
}
]
})
</script>
<template>
<ContentWrap title="useValidator">
<Form :schema="formSchema" :rules="rules" @register="formRegister" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { useWatermark } from '@/hooks/web/useWatermark'
import { computed, onBeforeUnmount } from 'vue'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
const { setWatermark, clear } = useWatermark()
const { t } = useI18n()
onBeforeUnmount(() => {
clear()
})
</script>
<template>
<ContentWrap title="useWatermark">
<BaseButton type="primary" @click="setWatermark(title)">
{{ t('watermarkDemo.createdWatermark') }}
</BaseButton>
<BaseButton type="danger" @click="clear">{{ t('watermarkDemo.clearWatermark') }}</BaseButton>
<BaseButton type="warning" @click="setWatermark(`New${title}`)">
{{ t('watermarkDemo.resetWatermark') }}
</BaseButton>
</ContentWrap>
</template>

View File

@@ -0,0 +1,209 @@
<template>
<div class="app-container">
<el-page-header @back="goBack" v-if="userId = parentId" />
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="时间">
<el-form-item label="下级玩家ID">
<el-input v-model="queryParams.playerId" clearable />
</el-form-item>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery"
>查询</el-button
>
</el-form-item>
</el-form>
<el-table
:data="alltableData"
style="width: 100%; margin-bottom: 20px; margin-top: 10px"
row-key="userId"
border
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="userId" label="用户ID" />
<el-table-column prop="userName" label="用户名" />
<el-table-column prop="nickName" label="昵称" />
<el-table-column prop="userType" label="用户类型">
<template #default="scope">
<span v-if="scope.row.userType === '01'">主播</span>
<span v-else-if="scope.row.userType === '02'">玩家</span>
<span v-else>未知类型</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column prop="totalRecharge" label="总充值" />
<el-table-column prop="totalNormGameCost" label="普通游戏消费" />
<el-table-column prop="totalSpecialGameCost" label="特殊游戏消费" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
v-if="scope.row.userType === '02'"
size="small"
type="primary"
link
icon="el-icon-money"
@click="getPurchaseData(scope.row.userId)"
>流水明细</el-button
>
<!-- <el-button
v-if="scope.row.userType === '01'"
size="small"
type="primary"
link
icon="el-icon-data-line"
@click="getCommissionRateData(scope.row.userId)"
>佣金比例</el-button
> -->
</template>
</el-table-column>
<!-- <el-table-column label="下级" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button size="small" link icon="el-icon-money" @click="getgengnduo(scope.row.userId)"
>查看下级</el-button
>
</template>
</el-table-column> -->
</el-table>
<el-pagination
layout="prev, pager, next"
:page-size="10"
:total="total"
@current-change="handleCurrentChange"
/>
<el-dialog title="流水明细" v-model="dialogTableVisible" align-center>
<Transaction :user-id="subFlowUserId" />
</el-dialog>
<el-dialog title="佣金比例" v-model="dialogCommissionRateVisible">
<span style="line-height: 30px">指定下级佣金比例设置后将无法修改</span>
<el-input v-model="editCommissionRateBody.commissionRate" placeholder="请输入佣金比例" />
<template v-slot:footer>
<div class="dialog-footer">
<el-button type="primary" size="default" @click="editCommissionRate">修改</el-button>
<el-button size="default" @click="dialogCommissionRateVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import {
ElPageHeader,
ElDatePicker,
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElInput,
ElButton,
ElMessage,
ElDialog,
ElPagination
} from 'element-plus'
import {
getSubBranches,
getPurchaseByUserId,
getCommissionRateByUserId,
updateCommissionRate
} from '@/api/subflow'
import Transaction from './transaction.vue'
import { getInfo } from '@/api/user'
// 定义响应式状态
const tableData = ref([])
const alltableData = ref([] as any)
const dialogTableVisible = ref(false)
const dialogCommissionRateVisible = ref(false)
const userId = ref('')
const parentId = ref('')
const adddd = ref([])
const subFlowUserId = ref('')
const total = ref(0)
const currentPage = ref(0)
// 定义查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
userId: undefined as number | undefined | string,
playerId: undefined as number | undefined | string
})
const editCommissionRateBody = reactive({
userId: '',
commissionRate: ''
})
const handleCurrentChange = (val: number) => {
currentPage.value = val
handleQuery()
}
/** 获取下级数据 */
const getSubBranchesData = () => {
queryParams.userId = userId.value
getSubBranches(queryParams).then((response) => {
tableData.value = response.data.rows
total.value = response.data.total
alltableData.value = tableData.value
})
}
/** 返回上一级 */
const goBack = () => {
/* console.log(alltableData.value) */
if (alltableData.value.length == 0) {
const a: any = tableData.value.filter((item: any) => item.userId === parentId.value)
console.log(a)
alltableData.value = tableData.value.filter((item: any) => item.parentId === a[0].parentId)
} else {
const a: any = tableData.value.filter(
(item: any) => item.userId === alltableData.value[0].parentId
)
console.log(a)
alltableData.value = tableData.value.filter((item: any) => item.parentId === a[0].parentId)
}
}
/** 获取采购数据 */
const getPurchaseData = (userId) => {
dialogTableVisible.value = true
subFlowUserId.value = userId
}
/** 获取并准备编辑佣金比例 */
const getCommissionRateData = (userId) => {
editCommissionRateBody.userId = userId
getCommissionRateByUserId(userId).then((response) => {
editCommissionRateBody.commissionRate = response.data.commissionRate
})
dialogCommissionRateVisible.value = true
}
/** 提交修改佣金比例 */
const editCommissionRate = () => {
updateCommissionRate(editCommissionRateBody).then((response) => {
ElMessage({
message: '修改成功',
type: 'success'
})
dialogCommissionRateVisible.value = false
})
}
const handleQuery = () => {
getInfo().then((res) => {
userId.value = res.data.userId
getSubBranchesData()
})
}
// 组件挂载时初始化
onMounted(() => {
handleQuery()
})
</script>

View File

@@ -0,0 +1,108 @@
<template>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery"
>查询</el-button
>
</el-form-item>
</el-form>
<el-table :data="gridData" style="width: 100%">
<el-table-column property="date" label="日期" width="150" />
<el-table-column property="dailyRecharge" label="日充值" width="200" />
<el-table-column property="dailyNormGameCost" label="日普通游戏消费" />
<el-table-column property="dailySpecialGameCost" label="日特殊游戏消费" />
</el-table>
<el-pagination
layout="prev, pager, next"
:page-size="10"
:total="total"
@current-change="handleCurrentChange"
/>
</template>
<script setup lang="ts">
import {
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElButton,
ElPagination,
ElDatePicker
} from 'element-plus'
import { getPurchaseByUserId } from '@/api/subflow/index'
import { onMounted, reactive, ref, watch } from 'vue'
// 接收父组件传递的数据
const props = defineProps<{
userId: string | number
}>()
const gridData = ref([])
// 定义查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
beginTime: undefined as string | undefined,
endTime: undefined as string | undefined,
userId: undefined as number | undefined | string
})
// 日期范围
const dateRange = ref([])
const total = ref(0)
const currentPage = ref(1)
watch(
() => props.userId,
(newVal) => {
if (newVal) {
handleQuery()
}
}
)
const handleCurrentChange = (val: number) => {
currentPage.value = val
handleQuery()
}
const getPurchaseData = () => {
getPurchaseByUserId(queryParams).then((response) => {
gridData.value = response.rows
total.value = response.total
})
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.userId = props.userId
queryParams.pageSize = 10
queryParams.pageNum = currentPage.value
if (dateRange.value !== null && dateRange.value.length === 2) {
queryParams.beginTime = dateRange.value[0]
queryParams.endTime = dateRange.value[1]
} else {
queryParams.beginTime = undefined
queryParams.endTime = undefined
}
getPurchaseData()
}
onMounted(() => {
handleQuery()
})
</script>