This commit is contained in:
秋帆
2024-11-03 16:37:10 +08:00
191 changed files with 7855 additions and 5796 deletions

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,10 @@ const BASE_URL = '/common'
export function listDeptTree(query: { description: string }) {
return http.get<TreeNodeData[]>(`${BASE_URL}/tree/dept`, query)
}
/** @desc 查询部门用户树 */
export function listDeptWithUsersTree(query: { description?: string, status: number }) {
return http.get<TreeNodeData[]>(`${BASE_URL}/tree/deptWithUsers`, query)
}
/** @desc 查询菜单树 */
export function listMenuTree(query: { description: string }) {
return http.get<TreeNodeData[]>(`${BASE_URL}/tree/menu`, query)

73
src/apis/open/app.ts Normal file
View File

@@ -0,0 +1,73 @@
import http from '@/utils/http'
const BASE_URL = '/open/app'
export interface AppResp {
id: string
name: string
appKey: string
status: string
expirationTime: string
appDesc: string
createUserString: string
updateUserString: string
}
export interface AppDetailResp {
id: string
name: string
appKey: string
status: string
expirationTime: string
appDesc: string
createTime: string
updateUser: string
updateTime: string
createUserString: string
updateUserString: string
}
export interface AppQuery {
name: string
appKey: string
sort: Array<string>
}
export interface AppPageQuery extends AppQuery, PageQuery {}
/** @desc 查询应用列表 */
export function listApp(query: AppPageQuery) {
return http.get<PageRes<AppResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询应用详情 */
export function getApp(id: string) {
return http.get<AppDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增应用 */
export function addApp(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改应用 */
export function updateApp(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除应用 */
export function deleteApp(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 导出应用 */
export function exportApp(query: AppQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 查看AK */
export function getAppSecret(id: string) {
return http.get(`${BASE_URL}/${id}/appsecret`)
}
/** @desc 刷新AK */
export function refreshAppSecret(id: string) {
return http.get(`${BASE_URL}/${id}/refreshas`)
}

View File

@@ -29,3 +29,11 @@ export function updateRole(data: any, id: string) {
export function deleteRole(ids: string | Array<string>) {
return http.del(`${BASE_URL}/${ids}`)
}
/** @desc 获取角色绑定的用户列表 */
export function listRoleUsers(id: string) {
return http.get(`${BASE_URL}/listRoleUsers/${id}`)
}
export function bindUsers(id: string, userIds: Array<string>) {
return http.post(`${BASE_URL}/bindUsers/${id}`, userIds)
}

View File

@@ -40,6 +40,7 @@ export interface UserQuery {
createTime?: Array<string>
deptId?: string
sort: Array<string>
userIds?: Array<string>
}
export interface UserPageQuery extends UserQuery, PageQuery {
@@ -156,7 +157,7 @@ export interface DictQuery {
sort: Array<string>
}
export type DictItemResp = {
export interface DictItemResp {
id: string
label: string
value: string
@@ -183,17 +184,19 @@ export interface DictItemPageQuery extends DictItemQuery, PageQuery {
/** 系统公告类型 */
export interface NoticeResp {
id: string
title: string
id?: string
title?: string
content: string
status: number
type: string
effectiveTime: string
terminateTime: string
createUserString: string
createTime: string
updateUserString: string
updateTime: string
status?: number
type?: string
effectiveTime?: string
terminateTime?: string
noticeScope?: number
noticeUsers?: Array<string>
createUserString?: string
createTime?: string
updateUserString?: string
updateTime?: string
}
export interface NoticeQuery {
@@ -206,7 +209,7 @@ export interface NoticePageQuery extends NoticeQuery, PageQuery {
}
/** 系统文件类型 */
export type FileItem = {
export interface FileItem {
id: string
name: string
size: number
@@ -242,7 +245,7 @@ export interface FilePageQuery extends FileQuery, PageQuery {
}
/** 系统存储类型 */
export type StorageResp = {
export interface StorageResp {
id: string
name: string
code: string

View File

@@ -9,6 +9,9 @@ const BASE_URL = '/system/user'
export function listUser(query: T.UserPageQuery) {
return http.get<PageRes<T.UserResp[]>>(`${BASE_URL}`, query)
}
export function listAllUser(query: Partial<T.UserPageQuery>) {
return http.get<T.UserResp[]>(`${BASE_URL}/list`, query)
}
/** @desc 查询用户详情 */
export function getUser(id: string) {
@@ -41,13 +44,13 @@ export function resetUserPwd(data: any, id: string) {
}
/** @desc 下载用户导入模板 */
export function downloadImportUserTemplate() {
return http.download(`${BASE_URL}/downloadImportUserTemplate`)
export function downloadUserImportTemplate() {
return http.download(`${BASE_URL}/import/template`)
}
/** @desc 解析用户导入数据 */
export function parseImportUser(data: FormData) {
return http.post(`${BASE_URL}/parseImportUser`, data)
return http.post(`${BASE_URL}/import/parse`, data)
}
/** @desc 导入用户 */

View File

@@ -36,7 +36,7 @@ export function generate(tableNames: Array<string>) {
return http.requestNative({
url: `${BASE_URL}/${tableNames}`,
method: 'post',
responseType: 'blob'
responseType: 'blob',
})
}

View File

@@ -0,0 +1,6 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0H10C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20H12C17.5228 20 22 15.5228 22 10C22 4.47715 17.5228 0 12 0Z" fill="white"/>
<path d="M7.59998 6.6001V13.4001" stroke="#165DFF" stroke-width="1.2" stroke-linecap="round"/>
<path d="M11.1 6.6001V13.4001" stroke="#165DFF" stroke-width="1.2" stroke-linecap="round"/>
<path d="M14.6 6.6001V13.4001" stroke="#165DFF" stroke-width="1.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,82 @@
<template>
<a-avatar v-if="src" :size="size">
<img :src="src" :alt="alt" />
<template v-if="trigger" #trigger-icon><slot name="trigger-icon"></slot></template>
</a-avatar>
<a-avatar
v-else-if="name || text"
:size="size"
:style="{
backgroundColor: avatarColor,
}"
>
<span v-if="name">{{ avatarName }}</span>
<span v-else>{{ text }}</span>
<template v-if="trigger" #trigger-icon><slot name="trigger-icon"></slot></template>
</a-avatar>
<a-avatar v-else :size="size">
<img :src="Unknown" :alt="alt" />
<template v-if="trigger" #trigger-icon><slot name="trigger-icon"></slot></template>
</a-avatar>
</template>
<script lang="ts" setup>
import Unknown from '@/assets/images/avatar/unknown.png'
import * as Regexp from '@/utils/regexp'
defineOptions({ name: 'Avatar' })
const props = withDefaults(defineProps<Props>(), {
color: '#168CFF',
size: 20,
alt: 'avatar',
trigger: false,
})
interface Props {
src?: string
name?: string
text?: string
color?: string
size?: string | number
alt?: string
trigger?: boolean
}
/**
* 英文开头取传入字符串的前面两个字符例如Charles => Ch
* 超过 4 位:取字符串第一个字符,例如:系统管理员 => 系
* 其他:取传入字符串的最后两个字符,例如:张三 => 张三;王多鱼:多鱼
*/
const avatarName = computed(() => {
const name = props.name
if (!name) return ''
if (name[0].match(Regexp.OnlyEn)) {
const nameArr = name.split(' ')
if (nameArr.length > 1 && nameArr[1][0].match(Regexp.OnlyEn)) return `${nameArr[0][0]}${nameArr[1][0]}`
return name.substring(0, 2)
}
if (name.length > 4) return name[0]
return name.substring(name.length - 2, name.length)
})
const colors = [
'#168CFF',
'#7BC616',
'#14C9C9',
'#FF7D00',
'#FFC72E',
]
const avatarColor = computed(() => {
const hash = (s) => {
let hash = 0
for (let i = 0; i < s.length; i++) {
hash = (hash << 5) - hash + s.charCodeAt(i)
}
return Math.abs(hash)
}
return colors[hash(props.name || props.text) % (colors.length)]
})
</script>
<style scoped lang="less"></style>

View File

@@ -13,20 +13,20 @@ defineProps({
type: Object,
default() {
return {}
}
},
},
autoResize: {
type: Boolean,
default: true
default: true,
},
width: {
type: String,
default: '100%'
default: '100%',
},
height: {
type: String,
default: '100%'
}
default: '100%',
},
})
registerMap('world', worldMap)
registerMap('china', chinaMap)

View File

@@ -19,47 +19,47 @@ defineOptions({ name: 'DateRangePicker' })
defineProps({
format: {
type: String,
default: 'YYYY-MM-DD HH:mm:ss'
default: 'YYYY-MM-DD HH:mm:ss',
},
showTime: {
type: Boolean,
default: true
default: true,
},
placeholder: {
type: Array as PropType<string[]>,
default: (): string[] => ['开始时间', '结束时间']
default: (): string[] => ['开始时间', '结束时间'],
},
allowClear: {
type: Boolean,
default: true
}
default: true,
},
})
const shortcuts = computed<ShortcutType[]>(() => {
return [
{
label: '今天',
value: (): Date[] => [dayjs().startOf('day').toDate(), dayjs().toDate()]
value: (): Date[] => [dayjs().startOf('day').toDate(), dayjs().toDate()],
},
{
label: '昨天',
value: (): Date[] => [
dayjs().subtract(1, 'day').startOf('day').toDate(),
dayjs().subtract(1, 'day').endOf('day').toDate()
]
dayjs().subtract(1, 'day').endOf('day').toDate(),
],
},
{
label: '本周',
value: (): Date[] => [dayjs().startOf('week').add(1, 'day').toDate(), dayjs().toDate()]
value: (): Date[] => [dayjs().startOf('week').add(1, 'day').toDate(), dayjs().toDate()],
},
{
label: '本月',
value: (): Date[] => [dayjs().startOf('month').toDate(), dayjs().toDate()]
value: (): Date[] => [dayjs().startOf('month').toDate(), dayjs().toDate()],
},
{
label: '本年',
value: (): Date[] => [dayjs().startOf('year').toDate(), dayjs().toDate()]
}
value: (): Date[] => [dayjs().startOf('year').toDate(), dayjs().toDate()],
},
]
})
</script>

View File

@@ -1,11 +1,11 @@
<template>
<a-modal
v-model:visible="visible"
:width="width >= 1350 ? 1350 : '100%'"
:on-before-close="onClose"
:footer="false"
esc-to-close="esc-to-close"
@close="onClose"
v-model:visible="visible"
:width="width >= 1350 ? 1350 : '100%'"
:on-before-close="onClose"
:footer="false"
esc-to-close="esc-to-close"
@close="onClose"
>
<template #title>
{{ modalTitle }}
@@ -18,26 +18,26 @@
<a-spin :loading="loading" class="w-full mt--10">
<a-card class="preview-content">
<VueOfficePdf
v-if="filePreview.fileInfo?.fileType === 'pdf'"
:src="filePreview.fileInfo?.data"
class="h-full"
@rendered="renderedHandler"
@error="errorHandler"
v-if="filePreview.fileInfo?.fileType === 'pdf'"
:src="filePreview.fileInfo?.data"
class="h-full"
@rendered="renderedHandler"
@error="errorHandler"
/>
<VueOfficeDocx
v-else-if="WordTypes.includes(filePreview.fileInfo?.fileType || '')"
:src="filePreview.fileInfo?.data"
class="h-full"
@rendered="renderedHandler"
@error="errorHandler"
v-else-if="WordTypes.includes(filePreview.fileInfo?.fileType || '')"
:src="filePreview.fileInfo?.data"
class="h-full"
@rendered="renderedHandler"
@error="errorHandler"
/>
<VueOfficeExcel
v-else-if="ExcelTypes.includes(filePreview.fileInfo?.fileType || '')"
:src="filePreview.fileInfo?.data"
style="height: 80vh; width: 100%"
:options="filePreview.excelConfig"
@rendered="renderedHandler"
@error="errorHandler"
v-else-if="ExcelTypes.includes(filePreview.fileInfo?.fileType || '')"
:src="filePreview.fileInfo?.data"
style="height: 80vh; width: 100%"
:options="filePreview.excelConfig"
@rendered="renderedHandler"
@error="errorHandler"
/>
</a-card>
</a-spin>
@@ -65,7 +65,7 @@ const blobUrl = ref<string>('')
// 文件预览对象
const filePreview = reactive<FilePreview>({
fileInfo: {},
excelConfig: {}
excelConfig: {},
})
// 弹框标题
const modalTitle = computed(() => {
@@ -124,7 +124,7 @@ const onOpen = () => {
const onClose = () => {
Object.assign(filePreview, {
fileInfo: {},
excelConfig: {}
excelConfig: {},
})
loading.value = false
visible.value = false

View File

@@ -53,8 +53,8 @@ export default defineComponent({
props: useFormProps({
defaultValue: '*',
props: {
week: { type: String, default: '?' }
}
week: { type: String, default: '?' },
},
}),
emits: useFromEmits(),
setup(props, context) {
@@ -68,21 +68,21 @@ export default defineComponent({
maxValue: 31,
valueRange: { start: 1, end: 31 },
valueLoop: { start: 1, interval: 1 },
disabled: isDisabled
disabled: isDisabled,
})
const typeWorkAttrs = computed(() => ({
disabled: setup.type.value !== TypeEnum.work || props.disabled || isDisabled.value,
...setup.inputNumberAttrs.value
...setup.inputNumberAttrs.value,
}))
watch(
() => props.week,
() => {
setup.updateValue(isDisabled.value ? '?' : setup.computeValue.value)
}
},
)
return { ...setup, typeWorkAttrs }
}
},
})
</script>

View File

@@ -44,7 +44,7 @@ import { useFormProps, useFormSetup, useFromEmits } from './use-mixin'
export default defineComponent({
name: 'HourForm',
props: useFormProps({
defaultValue: '*'
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
@@ -53,8 +53,8 @@ export default defineComponent({
minValue: 0,
maxValue: 23,
valueRange: { start: 0, end: 23 },
valueLoop: { start: 0, interval: 1 }
valueLoop: { start: 0, interval: 1 },
})
}
},
})
</script>

View File

@@ -44,7 +44,7 @@ import { useFormProps, useFormSetup, useFromEmits } from './use-mixin'
export default defineComponent({
name: 'MinuteForm',
props: useFormProps({
defaultValue: '*'
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
@@ -53,8 +53,8 @@ export default defineComponent({
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 }
valueLoop: { start: 0, interval: 1 },
})
}
},
})
</script>

View File

@@ -44,7 +44,7 @@ import { useFormProps, useFormSetup, useFromEmits } from './use-mixin'
export default defineComponent({
name: 'MonthForm',
props: useFormProps({
defaultValue: '*'
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
@@ -53,8 +53,8 @@ export default defineComponent({
minValue: 1,
maxValue: 12,
valueRange: { start: 1, end: 12 },
valueLoop: { start: 1, interval: 1 }
valueLoop: { start: 1, interval: 1 },
})
}
},
})
</script>

View File

@@ -44,7 +44,7 @@ import { useFormProps, useFormSetup, useFromEmits } from './use-mixin'
export default defineComponent({
name: 'SecondForm',
props: useFormProps({
defaultValue: '*'
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
@@ -53,8 +53,8 @@ export default defineComponent({
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 }
valueLoop: { start: 0, interval: 1 },
})
}
},
})
</script>

View File

@@ -8,7 +8,7 @@ export enum TypeEnum {
loop = 'LOOP',
work = 'WORK',
last = 'LAST',
specify = 'SPECIFY'
specify = 'SPECIFY',
}
// 周定义
@@ -19,7 +19,7 @@ export const WEEK_MAP: any = {
4: '周三',
5: '周四',
6: '周五',
7: '周六'
7: '周六',
}
// use 公共 props
@@ -28,13 +28,13 @@ export function useFormProps(options: any) {
return {
modelValue: {
type: String,
default: defaultValue
default: defaultValue,
},
disabled: {
type: Boolean,
default: false
default: false,
},
...options?.props
...options?.props,
}
}
@@ -166,7 +166,7 @@ export function useFormSetup(props: any, context: any, options: any) {
const beforeRadioAttrs = computed(() => ({
class: ['choice'],
disabled: props.disabled || unref(options.disabled),
size: 'small'
size: 'small',
}))
// 输入框属性
@@ -176,26 +176,26 @@ export function useFormSetup(props: any, context: any, options: any) {
precision: 0,
size: 'small',
hideButton: true,
class: 'w60'
class: 'w60',
}))
// 区间属性
const typeRangeAttrs = computed(() => ({
disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled),
...inputNumberAttrs.value
...inputNumberAttrs.value,
}))
// 间隔属性
const typeLoopAttrs = computed(() => ({
disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
...inputNumberAttrs.value
...inputNumberAttrs.value,
}))
// 指定属性
const typeSpecifyAttrs = computed(() => ({
disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled),
class: ['list-check-item'],
size: 'small'
size: 'small',
}))
return {
@@ -215,6 +215,6 @@ export function useFormSetup(props: any, context: any, options: any) {
inputNumberAttrs,
typeRangeAttrs,
typeLoopAttrs,
typeSpecifyAttrs
typeSpecifyAttrs,
}
}

View File

@@ -50,8 +50,8 @@ export default defineComponent({
props: useFormProps({
defaultValue: '?',
props: {
day: { type: String, default: '*' }
}
day: { type: String, default: '*' },
},
}),
emits: useFromEmits(),
setup(props, context) {
@@ -66,7 +66,7 @@ export default defineComponent({
// 0,7表示周日 1表示周一
valueRange: { start: 1, end: 7 },
valueLoop: { start: 2, interval: 1 },
disabled: disabledChoice
disabled: disabledChoice,
})
const weekOptions = computed(() => {
const options: { label: string, value: number }[] = []
@@ -74,7 +74,7 @@ export default defineComponent({
const weekName: string = WEEK_MAP[weekKey]
options.push({
value: Number.parseInt(weekKey),
label: weekName
label: weekName,
})
}
return options
@@ -83,13 +83,13 @@ export default defineComponent({
const typeRangeSelectAttrs = computed(() => ({
disabled: setup.typeRangeAttrs.value.disabled,
size: 'small',
class: ['w80']
class: ['w80'],
}))
const typeLoopSelectAttrs = computed(() => ({
disabled: setup.typeLoopAttrs.value.disabled,
size: 'small',
class: ['w80']
class: ['w80'],
}))
watch(() => props.day, () => {
@@ -101,8 +101,8 @@ export default defineComponent({
weekOptions,
typeLoopSelectAttrs,
typeRangeSelectAttrs,
WEEK_MAP
WEEK_MAP,
}
}
},
})
</script>

View File

@@ -30,7 +30,7 @@ import { useFormProps, useFormSetup, useFromEmits } from './use-mixin'
export default defineComponent({
name: 'YearForm',
props: useFormProps({
defaultValue: '*'
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
@@ -39,8 +39,8 @@ export default defineComponent({
defaultValue: '*',
minValue: 0,
valueRange: { start: nowYear, end: nowYear + 100 },
valueLoop: { start: nowYear, interval: 1 }
valueLoop: { start: nowYear, interval: 1 },
})
}
},
})
</script>

View File

@@ -95,9 +95,11 @@
</a-col>
<!-- 表达式 -->
<a-col :span="16">
<a-input v-model="cronInputs.cron"
:placeholder="placeholder"
@change="onInputCronChange">
<a-input
v-model="cronInputs.cron"
:placeholder="placeholder"
@change="onInputCronChange"
>
<template #prepend>
<span class="allow-click">表达式</span>
</template>
@@ -135,7 +137,7 @@ const props = withDefaults(defineProps<Partial<CronPropType>>(), {
disabled: false,
hideSecond: false,
hideYear: false,
placeholder: '请输入 Cron 表达式'
placeholder: '请输入 Cron 表达式',
})
const emit = defineEmits(['change', 'update:modelValue'])
const activeKey = ref(props.hideSecond ? 'minute' : 'second')
@@ -154,7 +156,7 @@ const cronInputs = reactive({
month: '',
week: '',
year: '',
cron: ''
cron: '',
})
const previewTimes = ref('执行预览')
@@ -190,7 +192,7 @@ const calculateNextExecutionTimes = (corn: string = cronExpression.value) => {
// 解析表达式
const date = dateFormat(new Date())
const iter = CronParser.parseExpression(parse, {
currentDate: date
currentDate: date,
})
const result: string[] = []
for (let i = 1; i <= 5; i++) {

View File

@@ -1,23 +1,27 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-small"
title-align="start"
title="Cron表达式生成"
:top="32"
:width="780"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:body-style="{ padding: '4px 16px 8px 16px' }">
<a-modal
v-model:visible="visible"
modal-class="modal-form-small"
title-align="start"
title="Cron表达式生成"
:top="32"
:width="780"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:body-style="{ padding: '4px 16px 8px 16px' }"
>
<!-- cron 输入框 -->
<CronGeneratorInput ref="cronInputRef" v-model="cronExpression" />
<!-- 页脚 -->
<template #footer>
<a-button size="small" @click="handlerClose">关闭</a-button>
<a-button size="small"
type="primary"
@click="handlerOk">
<a-button
size="small"
type="primary"
@click="handlerOk"
>
确定
</a-button>
</template>

View File

@@ -1,8 +1,6 @@
<template>
<a-space fill>
<a-avatar :size="24" shape="circle">
<img :src="props.avatar" alt="avatar" />
</a-avatar>
<Avatar :src="props.avatar" :name="props.name" :size="24" />
<a-link v-if="props.isLink" @click="emit('click')">
<a-typography-paragraph
class="link-text"
@@ -15,7 +13,17 @@
{{ props.name }}
</a-typography-paragraph>
</a-link>
<span v-else>{{ props.name }}</span>
<span v-else>
<a-typography-paragraph
:ellipsis="{
rows: 1,
showTooltip: true,
css: true,
}"
>
{{ props.name }}
</a-typography-paragraph>
</span>
</a-space>
</template>
@@ -25,7 +33,7 @@ defineOptions({ name: 'GiCellAvatar' })
const props = withDefaults(defineProps<Props>(), {
avatar: '',
name: '',
isLink: false // 是否可以点击
isLink: false, // 是否可以点击
})
const emit = defineEmits<{
@@ -33,7 +41,7 @@ const emit = defineEmits<{
}>()
interface Props {
avatar: string
avatar?: string
name: string
isLink?: boolean
}

View File

@@ -16,7 +16,7 @@
defineOptions({ name: 'GiCellGender' })
const props = withDefaults(defineProps<Props>(), {
gender: 1
gender: 1,
})
interface Props {

View File

@@ -13,7 +13,7 @@
defineOptions({ name: 'GiCellStatus' })
const props = withDefaults(defineProps<Props>(), {
status: 1
status: 1,
})
interface Props {

View File

@@ -17,15 +17,15 @@ defineOptions({ name: 'GiCellTag' })
const props = withDefaults(defineProps<Partial<GiCellTagType>>(), {
dict: [{
label: '',
value: ''
value: '',
}],
value: ''
value: '',
})
const dictItem = computed((): LabelValueState => {
try {
return props.dict.find(
(d) => d.value === String(props.value) || d.value === Number(props.value)
(d) => d.value === String(props.value) || d.value === Number(props.value),
) || { label: '', value: '' }
} catch (error) {
return { label: '', value: '' }

View File

@@ -22,7 +22,7 @@
defineOptions({ name: 'GiCellTags' })
withDefaults(defineProps<Props>(), {
data: () => []
data: () => [],
})
interface Props {

View File

@@ -19,7 +19,7 @@ import { useAppStore } from '@/stores'
const props = withDefaults(defineProps<Props>(), {
type: 'javascript',
codeJson: ''
codeJson: '',
})
const appStore = useAppStore()
const isDark = computed(() => appStore.theme === 'dark')
@@ -32,7 +32,7 @@ const defaultConfig = {
tabSize: 2,
basic: true,
dark: true,
readonly: true
readonly: true,
}
const config = defaultConfig

View File

@@ -7,14 +7,14 @@ export default defineComponent({
props: {
animation: {
type: Boolean,
default: true
default: true,
},
type: {
type: String as PropType<TPropsType>,
default: 'primary'
}
default: 'primary',
},
},
setup(props) {
return () => <span class={['gi-dot', { 'gi-dot-processing': props.animation }, `gi-dot-${props.type}`]}></span>
}
},
})

View File

@@ -0,0 +1,127 @@
<template>
<div class="gi-edit-table">
<a-form ref="formRef" :model="form">
<a-table :data="form.tableData" :bordered="{ cell: true }" :pagination="false" v-bind="attrs">
<template #columns>
<a-table-column
v-for="col in props.columns" :key="col.dataIndex" :title="col.title"
:data-index="col.dataIndex" :header-cell-class="headerCellClass(col)" v-bind="col.columnProps"
>
<template #cell="{ record, rowIndex, column }">
<a-form-item
:field="`tableData[${rowIndex}].${col.dataIndex}`" :label-col-style="{ display: 'none' }"
:wrapper-col-props="{ span: 24 }" v-bind="col.formItemProps"
:rules="[{ required: col.required || false, message: getRuleMessage(col) }, ...(col.rules || [])]"
>
<template v-if="col.slotName">
<slot :name="col.dataIndex" v-bind="{ record, rowIndex, column }"></slot>
</template>
<component
:is="`a-${col.type}`" v-else v-bind="getComponentBindProps(col)"
v-model="record[col.dataIndex]" :disabled="isDisabled({ row: record, rowIndex, col })"
>
</component>
</a-form-item>
</template>
</a-table-column>
</template>
</a-table>
</a-form>
</div>
</template>
<script lang='ts' setup generic="T extends TableData">
import type { TableData } from '@arco-design/web-vue'
import type { ColumnItem, Disabled } from './type'
defineOptions({ name: 'GiEditTable', inheritAttrs: false })
const props = withDefaults(defineProps<Props>(), {
cellDisabled: false,
})
defineSlots<{
[propsName: string]: (props: { record: T, rowIndex: number, column: ColumnItem }) => void
}>()
interface Props {
columns: ColumnItem[]
data: T[]
cellDisabled?: Disabled<T>
}
const attrs = useAttrs()
const form = computed(() => ({ tableData: props.data }))
const formRef = useTemplateRef('formRef')
defineExpose({ formRef })
const headerCellClass = (col: ColumnItem) => {
return col.required ? 'gi_column_require' : ''
}
const getComponentBindProps = (col: ColumnItem) => {
const obj: Partial<ColumnItem['props'] & { placeholder: string }> = {}
if (col.type === 'input') {
obj.allowClear = true
obj.placeholder = `请输入${col.title}`
obj.maxLength = 50
}
if (col.type === 'input-number') {
obj.placeholder = `请输入${col.title}`
}
if (col.type === 'textarea') {
obj.allowClear = true
obj.placeholder = `填写${col.title}`
obj.maxLength = 200
}
if (col.type === 'select') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'cascader') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'tree-select') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'date-picker') {
obj.placeholder = '请选择日期'
}
if (col.type === 'time-picker') {
obj.allowClear = true
obj.placeholder = `请选择时间`
}
return { ...obj, ...col.props }
}
const getRuleMessage = (col: ColumnItem) => {
if (['input', 'input-number'].includes(col.type ?? '')) {
return `请输入${col.title}`
}
if (['textarea'].includes(col.type ?? '')) {
return `请填写${col.title}`
}
if (['select', 'cascader', 'tree-select'].includes(col.type ?? '')) {
return `请选择${col.title}`
}
if (['date-picker'].includes(col.type ?? '')) {
return `请选择日期`
}
if (['time-picker'].includes(col.type ?? '')) {
return `请选择时间`
}
return ''
}
const isDisabled: Props['cellDisabled'] = (p) => {
if (typeof props?.cellDisabled === 'boolean') return props.cellDisabled
if (typeof props?.cellDisabled === 'function') return props.cellDisabled(p)
return false
}
</script>
<style lang='scss' scoped></style>

View File

@@ -0,0 +1,5 @@
import GiEditTable from './GiEditTable.vue'
export type * from './type'
export default GiEditTable

View File

@@ -0,0 +1,51 @@
import type * as A from '@arco-design/web-vue'
export interface ColumnItem {
type?:
| 'input'
| 'select'
| 'radio-group'
| 'checkbox-group'
| 'textarea'
| 'date-picker'
| 'year-picker'
| 'quarter-picker'
| 'week-picker'
| 'range-picker'
| 'month-picker'
| 'time-picker'
| 'color-picker'
| 'input-number'
| 'rate'
| 'switch'
| 'slider'
| 'cascader'
| 'tree-select'
| 'upload'
| ''
title: string
dataIndex: string
required?: boolean
rules?: A.FormItemInstance['$props']['rules'] // 表单校验规则
props?:
& A.InputInstance['$props']
& A.SelectInstance['$props']
& A.TextareaInstance['$props']
& A.DatePickerInstance['$props']
& A.TimePickerInstance['$props']
& A.RadioGroupInstance['$props']
& A.CheckboxGroupInstance['$props']
& A.InputNumberInstance['$props']
& A.RateInstance['$props']
& A.SwitchInstance['$props']
& A.SliderInstance['$props']
& A.CascaderInstance['$props']
& A.TreeSelectInstance['$props']
& A.UploadInstance['$props']
& A.AlertInstance['$props']
columnProps?: A.TableColumnInstance['$props']
formItemProps?: A.FormItemInstance['$props']
slotName?: string
}
export type Disabled<T> = boolean | ((e: { row: T, rowIndex: number, col: ColumnItem }) => boolean)

View File

@@ -11,7 +11,7 @@ defineOptions({ name: 'GiFlexibleBox' })
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
direction: 'right'
direction: 'right',
})
interface Props {

View File

@@ -1,29 +1,39 @@
<template>
<a-form ref="formRef" :auto-label-width="true" v-bind="options.form" :model="modelValue">
<a-row :gutter="14" v-bind="options.row" class="w-full">
<a-grid class="w-full" :col-gap="8" v-bind="options.grid" :collapsed="collapsed">
<template v-for="(item, index) in columns" :key="item.field">
<a-col v-if="!isHide(item.hide)" v-show="colVShow(index)" :span="item.span || 12"
v-bind="item.col || item.span ? item.col : options.col">
<a-form-item v-bind="item.item" :label="item.label" :field="item.field" :rules="item.rules"
:disabled="isDisabled(item.disabled)">
<slot v-if="!['group-title'].includes(item.type || '')" :name="item.field"
v-bind="{ disabled: isDisabled(item.disabled) }">
<a-grid-item
v-if="!isHide(item.hide)" v-show="colVShow(index)" v-bind="item.gridItemProps || props.options.gridItem"
:span="item.span || options.gridItem?.span"
>
<a-form-item
v-bind="item.formItemProps" :label="item.label" :field="item.field" :rules="item.rules"
:disabled="isDisabled(item.disabled)"
>
<slot
v-if="!['group-title'].includes(item.type || '')" :name="item.field"
v-bind="{ disabled: isDisabled(item.disabled) }"
>
<template v-if="item.type === 'range-picker'">
<DateRangePicker v-bind="(item.props as A.RangePickerInstance['$props'])"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)" />
<DateRangePicker
v-bind="(item.props as A.RangePickerInstance['$props'])"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"
/>
</template>
<component :is="`a-${item.type}`" v-else v-bind="getComponentBindProps(item)"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"></component>
<component
:is="`a-${item.type}`" v-else v-bind="getComponentBindProps(item)"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"
></component>
</slot>
<slot v-else name="group-title">
<a-alert v-bind="item.props">{{ item.label }}</a-alert>
</slot>
</a-form-item>
</a-col>
</a-grid-item>
</template>
<a-col v-if="!options.btns?.hide" :span="options.btns?.span || 12" v-bind="options.btns?.col">
<a-grid-item v-if="!options.btns?.hide" :suffix="options.fold?.enable">
<a-space wrap :size="[8, 16]" style="flex-wrap: nowrap">
<slot name="suffix">
<a-button type="primary" @click="emit('search')">
@@ -34,7 +44,10 @@
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
<a-button v-if="options.fold?.enable" type="text" size="mini" @click="collapsed = !collapsed">
<a-button
v-if="options.fold?.enable" class="gi-form__fold-btn" type="text" size="mini"
@click="collapsed = !collapsed"
>
<template #icon>
<icon-up v-if="!collapsed" />
<icon-down v-else />
@@ -43,23 +56,24 @@
</a-button>
</slot>
</a-space>
</a-col>
</a-row>
</a-grid-item>
</a-grid>
</a-form>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'
import type { Columns, ColumnsItem, ColumnsItemDisabled, ColumnsItemHide, Options } from './type'
import DateRangePicker from '@/components/DateRangePicker/index.vue'
import type { ColumnsItem, ColumnsItemDisabled, ColumnsItemHide, Options } from './type'
interface Props {
modelValue: any
options: Options
columns: Columns
options?: Options
columns: ColumnsItem[]
}
const props = withDefaults(defineProps<Props>(), {})
const props = withDefaults(defineProps<Props>(), {
options: () => ({}),
})
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
@@ -67,7 +81,14 @@ const emit = defineEmits<{
(e: 'reset'): void
}>()
const formRef = ref('formRef')
const options = computed(() => ({
grid: { cols: 1 },
gridItem: { span: { xs: 2, sm: 1 } },
...props.options,
}
))
const formRef = useTemplateRef('formRef')
const collapsed = ref(props.options.fold?.defaultCollapsed ?? false)
const dicData: Record<string, any> = reactive({})
@@ -80,12 +101,15 @@ const colVShow = (index: number) => {
const getComponentBindProps = (item: ColumnsItem) => {
const obj: Partial<ColumnsItem['props'] & { placeholder: string }> = {}
if (item.type === 'input') {
obj.allowClear = true
obj.placeholder = `请输入${item.label}`
}
if (item.type === 'input-password') {
obj.allowClear = true
obj.placeholder = `请输入${item.label}`
}
if (item.type === 'input-number') {
obj.allowClear = true
obj.placeholder = `请输入${item.label}`
}
if (item.type === 'textarea') {
@@ -93,14 +117,17 @@ const getComponentBindProps = (item: ColumnsItem) => {
obj.maxLength = 200
}
if (item.type === 'select') {
obj.allowClear = true
obj.placeholder = `请选择${item.label}`
obj.options = dicData[item.field] || item.options
}
if (item.type === 'cascader') {
obj.allowClear = true
obj.placeholder = `请选择${item.label}`
obj.options = dicData[item.field] || item.options
}
if (item.type === 'tree-select') {
obj.allowClear = true
obj.placeholder = `请选择${item.label}`
obj.data = dicData[item.field] || item.data
}
@@ -146,6 +173,7 @@ props.columns.forEach((item) => {
if (item.request && typeof item.request === 'function' && item?.init) {
item.request(props.modelValue).then((res) => {
dicData[item.field] = item.resultFormat ? item.resultFormat(res) : res.data
// console.log('dicData', dicData)
})
}
})
@@ -191,4 +219,7 @@ watch(cloneForm as any, (newVal, oldVal) => {
:deep(.arco-form-item-layout-inline) {
margin-right: 0;
}
.gi-form__fold-btn {
padding: 0 5px;
}
</style>

View File

@@ -43,6 +43,6 @@ export function useGiForm(initValue: Columns) {
/** 设置 columns 某个对象属性的值 */
setValue,
/** 设置 columns.props 某个属性的值 */
setPropsValue
setPropsValue,
}
}

View File

@@ -71,9 +71,8 @@ export interface ColumnsItem<F = any> {
type?: FormType // 类型
label?: A.FormItemInstance['label'] // 标签
field: A.FormItemInstance['field'] // 字段(必须唯一)
span?: number // 栅格占位格数
col?: A.ColProps // a-col的props, 响应式布局, 优先级大于span
item?: Omit<A.FormItemInstance['$props'], 'label' | 'field'> // a-form-item的props
gridItemProps?: A.GridItemProps
formItemProps?: Omit<A.FormItemInstance['$props'], 'label' | 'field'> // a-form-item的props
props?:
& A.InputInstance['$props']
& A.InputPasswordInstance['$props']
@@ -99,6 +98,7 @@ export interface ColumnsItem<F = any> {
| A.CheckboxGroupInstance['$props']['options']
| A.CascaderInstance['$props']['options']
// 下拉树组件的data
span?: A.GridItemProps['span']
data?: A.TreeSelectInstance['$props']['data']
hide?: ColumnsItemHide<F> // 是否隐藏
disabled?: ColumnsItemDisabled<F> // 是否禁用
@@ -109,10 +109,10 @@ export interface ColumnsItem<F = any> {
}
export interface Options {
form: Omit<A.FormInstance['$props'], 'model'>
row?: Partial<typeof import('@arco-design/web-vue')['Row']['__defaults']>
col?: A.ColProps
btns?: { hide?: boolean, span?: number, col?: A.ColProps, searchBtnText?: string }
form?: Omit<A.FormInstance['$props'], 'model'>
grid?: A.GridProps
gridItem?: A.GridItemProps
btns?: { hide?: boolean, searchBtnText?: string }
fold?: { enable?: boolean, index?: number, defaultCollapsed?: boolean }
}

View File

@@ -74,7 +74,7 @@ defineOptions({ name: 'GiIconSelector' })
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
enableCopy: false
enableCopy: false,
})
const emit = defineEmits(['select', 'update:modelValue'])

View File

@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
icon: '',
label: '',
more: false,
active: false
active: false,
})
const emit = defineEmits<{

View File

@@ -22,7 +22,7 @@
defineOptions({ name: 'GiOverFlowTags' })
withDefaults(defineProps<Props>(), {
data: () => []
data: () => [],
})
interface Props {
data: string[]

View File

@@ -15,7 +15,7 @@ defineOptions({ name: 'GiSvgIcon' })
const props = withDefaults(defineProps<Props>(), {
name: '',
color: '',
size: 20
size: 20,
})
interface Props {

View File

@@ -34,8 +34,10 @@
</a-doption>
</template>
</a-dropdown>
<a-popover v-if="showSettingColumnBtn" trigger="click" position="br"
:content-style="{ minWidth: '120px', padding: '6px 8px 10px' }">
<a-popover
v-if="showSettingColumnBtn" trigger="click" position="br"
:content-style="{ minWidth: '120px', padding: '6px 8px 10px' }"
>
<a-tooltip content="列设置">
<a-button>
<template #icon>
@@ -73,8 +75,16 @@
</a-row>
<div class="gi-table__body" :class="`gi-table__body-pagination-${attrs['page-position']}`">
<div class="gi-table__container">
<a-table ref="tableRef" :stripe="stripe" :size="size" column-resizable :bordered="{ cell: isBordered }"
v-bind="{ ...attrs, columns: _columns }" :scrollbar="true">
<a-table
ref="tableRef"
:stripe="stripe"
:size="size"
column-resizable
:bordered="{ cell: isBordered }"
v-bind="{ ...attrs, columns: _columns }"
:scrollbar="true"
:data="data"
>
<template v-for="key in Object.keys(slots)" :key="key" #[key]="scoped">
<slot :key="key" :name="key" v-bind="scoped"></slot>
</template>
@@ -84,26 +94,46 @@
</div>
</template>
<script setup lang="ts">
import type { DropdownInstance, TableColumnData, TableInstance } from '@arco-design/web-vue'
<script setup lang="ts" generic="T extends TableData">
import type { DropdownInstance, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue'
import { VueDraggable } from 'vue-draggable-plus'
defineOptions({ name: 'GiTable', inheritAttrs: false })
const props = withDefaults(defineProps<Props>(), {
title: '',
data: () => [],
disabledTools: () => [], // 禁止显示的工具
disabledColumnKeys: () => [] // 禁止控制显示隐藏的列
disabledColumnKeys: () => [], // 禁止控制显示隐藏的列
})
const emit = defineEmits<{
(e: 'refresh'): void
}>()
defineSlots<{
'th': (props: { column: TableColumnData }) => void
'thead': () => void
'empty': (props: { column: TableColumnData }) => void
'summary-cell': (props: { column: TableColumnData, record: T, rowIndex: number }) => void
'pagination-right': () => void
'pagination-left': () => void
'td': (props: { column: TableColumnData, record: T, rowIndex: number }) => void
'tr': (props: { record: T, rowIndex: number }) => void
'tbody': () => void
'drag-handle-icon': () => void
'footer': () => void
'expand-row': (props: { record: T }) => void
'expand-icon': (props: { record: T, expanded?: boolean }) => void
'columns': () => void
[propsName: string]: (props: { key: string, record: T, column: TableColumnData, rowIndex: number }) => void
}>()
const attrs = useAttrs()
const slots = useSlots()
interface Props {
title?: string
data: T[]
disabledTools?: string[]
disabledColumnKeys?: string[]
}
@@ -114,10 +144,10 @@ const size = ref<TableInstance['size']>('medium')
const isBordered = ref(false)
const isFullscreen = ref(false)
type SizeItem = { label: string, value: TableInstance['size'] }
interface SizeItem { label: string, value: TableInstance['size'] }
const sizeList: SizeItem[] = [
{ label: '紧凑', value: 'small' },
{ label: '默认', value: 'medium' }
{ label: '默认', value: 'medium' },
]
const handleSelect: DropdownInstance['onSelect'] = (value) => {
@@ -132,9 +162,9 @@ const showRefreshBtn = computed(() => !props.disabledTools.includes('refresh'))
const showSizeBtn = computed(() => !props.disabledTools.includes('size'))
const showFullscreenBtn = computed(() => !props.disabledTools.includes('fullscreen'))
const showSettingColumnBtn = computed(
() => !props.disabledTools.includes('setting') && attrs?.columns && (attrs?.columns as TableColumnData[])?.length
() => !props.disabledTools.includes('setting') && attrs?.columns && (attrs?.columns as TableColumnData[])?.length,
)
type SettingColumnItem = { title: string, key: string, show: boolean, disabled: boolean }
interface SettingColumnItem { title: string, key: string, show: boolean, disabled: boolean }
const settingColumnList = ref<SettingColumnItem[]>([])
// 重置配置列
@@ -148,8 +178,8 @@ const resetSettingColumns = () => {
title: typeof item.title === 'string' ? item.title : '',
show: item.show ?? true,
disabled: props.disabledColumnKeys.includes(
item.dataIndex || (typeof item.title === 'string' ? item.title : '')
)
item.dataIndex || (typeof item.title === 'string' ? item.title : ''),
),
})
})
}
@@ -160,7 +190,7 @@ watch(
() => {
resetSettingColumns()
},
{ immediate: true }
{ immediate: true },
)
// 排序和过滤可显示的列数据
@@ -173,7 +203,7 @@ const _columns = computed(() => {
.map((i) => i.key || (typeof i.title === 'string' ? i.title : ''))
// 显示的columns数据
const filterColumns = arr.filter((i) =>
showDataIndexs.includes(i.dataIndex || (typeof i.title === 'string' ? i.title : ''))
showDataIndexs.includes(i.dataIndex || (typeof i.title === 'string' ? i.title : '')),
)
const sortedColumns: TableColumnData[] = []
settingColumnList.value.forEach((i) => {

View File

@@ -16,7 +16,7 @@ const baseColorObj = {
blue: '#3491fa',
purple: '#722ed1',
pink: '#f5319d',
gray: '#86909c'
gray: '#86909c',
}
type BaseColor = keyof typeof baseColorObj
@@ -26,24 +26,24 @@ export default defineComponent({
props: {
type: {
type: String as PropType<PropsType>,
default: 'light'
default: 'light',
},
status: {
type: String as PropType<PropsStatus>,
default: 'primary'
default: 'primary',
},
color: {
type: String as PropType<BaseColor | string>,
default: ''
default: '',
},
size: {
type: String as PropType<PropsSize>,
default: 'small'
default: 'small',
},
closable: {
type: Boolean,
default: false
}
default: false,
},
},
emits: ['click', 'close'],
setup(props, { slots, emit }) {
@@ -125,5 +125,5 @@ export default defineComponent({
{props.closable && CloseIcon}
</span>
)
}
},
})

View File

@@ -22,7 +22,7 @@ const isDark = useDark({
storageKey: 'arco-theme',
onChanged(dark: boolean) {
appStore.toggleTheme(dark)
}
},
})
const toggleTheme = useToggle(isDark)

View File

@@ -0,0 +1,238 @@
<template>
<div class="container">
<a-row :gutter="16">
<a-col :span="24" :md="5" class="section">
<a-input v-model="searchKey" placeholder="请输入部门名称" allow-clear>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-tree
ref="treeRef"
:data="treeData"
block-node
@select="handleDeptSelect"
/>
</a-col>
<a-col :span="24" :md="14" class="section">
<GiTable
v-model:selectedKeys="selectedKeys"
style="min-height: 600px;"
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%' }"
:pagination="pagination"
:disabled-tools="['size', 'fullscreen', 'setting', 'refresh']"
:row-selection="{ type: props.multiple ? 'checkbox' : 'radio', showCheckedAll: true }"
@select="onRowSelect"
@select-all="onTableSelectAll"
@refresh="search"
>
<template #top>
<div>
<a-space class="mt-5">
<a-input v-model="queryForm.description" placeholder="用户名/昵称/描述" />
<a-button @click="search">
<template #icon>
<icon-search />
</template>
</a-button>
<a-button @click="onRefresh">
<template #icon>
<icon-refresh />
</template>
</a-button>
</a-space>
</div>
<a-alert class="mt-5">
<template v-if="selectedKeys.length > 0">
已选中{{ selectedKeys.length }}条记录(可跨页)
</template>
<template v-else>
未选中任何项目
</template>
<template v-if="selectedKeys.length > 0" #action>
<a-link @click="onClearSelected">清空</a-link>
</template>
</a-alert>
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
</template>
</GiTable>
</a-col>
<a-col :span="24" :md="5" class="section">
<a-card title="已选用户">
<a-table :columns="rightColumn" :data="selectedData">
<template #action="{ record }">
<a-button @click="handleDeleteSelectUser(record)">
<icon-delete />
</a-button>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import type { TreeNodeData } from '@arco-design/web-vue'
import { useDept } from '@/hooks/app'
import { useTable } from '@/hooks'
import { listAllUser, listUser } from '@/apis'
import type { UserItem, UserSelectPropType } from '@/components/UserSelect/type'
const props = withDefaults(defineProps<UserSelectPropType & { selectedUsers: string | string[] }>(), {
multiple: false,
selectedUsers: () => [],
})
const emit = defineEmits(['update:selectedUsers'])
// 查询表单引用
const queryForm = ref({ description: '' })
// 部门树引用
const treeRef = ref()
const selectedKeys = ref<string[]>([])
const selectedDeptId = ref<string>('')
const selectedData = ref<any[]>([])
const { tableData: dataList, loading, pagination, search } = useTable(
(page) => listUser({ ...queryForm.value, deptId: selectedDeptId.value, sort: [], ...page }),
{ immediate: false, formatResult: (data) => data.map((i) => ({ ...i, disabled: false })) },
)
// 刷新表单
const onRefresh = () => {
queryForm.value.description = ''
search()
}
// 使用 useDept 钩子获取部门列表数据
const { deptList, getDeptList } = useDept({
onSuccess: () => {
nextTick(() => treeRef.value?.expandAll(true))
},
})
// 部门树过滤函数
const deptTreeSearch = (keyword: string, data: TreeNodeData[]): TreeNodeData[] => {
return data
.map((item) => ({
...item,
children: item.children ? deptTreeSearch(keyword, item.children) : [],
}))
.filter(
(item) =>
item.title?.toLowerCase().includes(keyword.toLowerCase()) || item.children?.length,
)
}
// 过滤树数据
const searchKey = ref('')
const treeData = computed(() => {
return searchKey.value ? deptTreeSearch(searchKey.value, deptList.value) : deptList.value
})
// 表格列定义
const tableColumns = [
{ title: '昵称', dataIndex: 'nickname' },
{ title: '部门', dataIndex: 'deptName' },
{ title: '角色', dataIndex: 'roleNames' },
{ title: '手机号', dataIndex: 'phone' },
{ title: '邮箱', dataIndex: 'email' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
]
// 右侧已选用户列定义
const rightColumn = [
{ title: '昵称', dataIndex: 'nickname' },
{ title: '操作', dataIndex: 'action', slotName: 'action' },
]
// 处理部门选择
const handleDeptSelect = (keys: Array<any>) => {
selectedDeptId.value = keys[0] || ''
search()
}
const emitSelectedUsers = () => {
emit('update:selectedUsers', selectedKeys.value)
}
// 从选中列表中移除用户
const handleDeleteSelectUser = (user: UserItem) => {
selectedData.value = selectedData.value.filter((item) => item.id !== user.id)
selectedKeys.value = selectedData.value.map((item) => item.id)
emitSelectedUsers()
}
// 行选择事件
const onRowSelect = (rowKeys: string[], rowKey: string, record: UserItem) => {
selectedData.value = props.multiple
? rowKeys.includes(rowKey)
? [...selectedData.value, record]
: selectedData.value.filter((item) => item.id !== rowKey)
: [record]
selectedKeys.value = selectedData.value.map((item) => item.id)
emitSelectedUsers()
}
// 全选事件
const onTableSelectAll = (checked: boolean) => {
selectedData.value = checked
? [...selectedData.value, ...dataList.value.filter((item) => !selectedKeys.value.includes(item.id))]
: []
selectedKeys.value = selectedData.value.map((item) => item.id)
emitSelectedUsers()
}
// 清空所有选中数据
const onClearSelected = () => {
selectedData.value = []
selectedKeys.value = []
emitSelectedUsers()
}
// 初始化函数
const init = (selectUsers: string[]) => {
getDeptList()
search()
if (selectUsers && selectUsers.length > 0) {
// admin的id是number 不是string 类型 所以处理一下
listAllUser({ userIds: selectUsers }).then((dataList) => {
selectedData.value = dataList.data.map((data) => {
return { ...data, id: `${data.id}` }
})
})
}
}
watch(() => props.selectedUsers, (newValue) => {
const newSelectedKeys = Array.isArray(newValue) ? newValue : [newValue]
selectedKeys.value = newSelectedKeys.filter(Boolean)
selectedData.value = dataList.value.filter((item) => selectedKeys.value.includes(item.id))
}, { immediate: true })
defineExpose({ init, onClearSelected })
</script>
<style scoped>
.container {
padding: 20px;
}
.section {
margin-bottom: 20px;
}
.mt-5 {
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div>
<div style="display: flex;">
<a-select
v-model="selectedUsers"
:allow-clear="true"
:multiple="props.multiple"
:max-tag-count="4"
:field-names="{ value: 'id', label: 'nickname' }"
:options="options"
@change="handleSelectChange"
/>
<a-tooltip content="选择用户">
<a-button @click="onOpen">
<template #icon>
<icon-plus />
</template>
</a-button>
</a-tooltip>
</div>
<a-modal
v-model:visible="visible"
title="用户选择"
:width="width >= 1350 ? 1350 : '100%'"
:esc-to-close="true"
@ok="handleModalOk"
>
<UserSelectContent
ref="userSelectContentRef"
:value="selectedUsers"
:multiple="props.multiple"
:selected-users="selectedUsers"
@update:selected-users="updateSelectedUsers"
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import UserSelectContent from './component/UserSelectContent.vue'
import { type UserResp, listAllUser } from '@/apis'
import type { UserSelectPropType } from '@/components/UserSelect/type'
const props = withDefaults(defineProps<UserSelectPropType>(), {
multiple: false, // 是否支持多选
value: '',
})
const emit = defineEmits(['update:value'])
const visible = ref<boolean>(false) // 控制弹窗显示的状态
const { width } = useWindowSize() // 获取窗口的宽度,用于设置弹窗宽度
const options = ref<UserResp[]>([]) // 保存用户选项列表
const userSelectContentRef = ref() // 引用 UserSelectContent 组件实例
const selectedUsers = ref<string[]>([]) // 保存已选择的用户
// 打开用户选择弹窗
const onOpen = () => {
visible.value = true
userSelectContentRef.value.init(selectedUsers.value) // 调用子组件的初始化方法
}
// 发出数据更新事件
const emitDataChange = () => {
emit('update:value', selectedUsers.value.filter(Boolean)) // 发出更新事件
}
// 处理用户选择变更事件
const handleSelectChange = (value: any) => {
selectedUsers.value = props.multiple ? value : [...value]
emitDataChange() // 每次选择变化时发出更新事件
}
// 更新已选择的用户列表
const updateSelectedUsers = (users: string[]) => {
selectedUsers.value = users
emitDataChange() // 每次选择变化时发出更新事件
}
// 弹窗确认按钮点击事件
const handleModalOk = () => {
emitDataChange() // 确认时发出数据更新事件
visible.value = false // 关闭弹窗
}
// 组件挂载后初始化用户列表
onMounted(async () => {
const { data } = await listAllUser({}) // 获取所有用户
options.value = data.map((user) => {
user.id = String(user.id)
user.disabled = false // 初始化时设置用户未被禁用
return user
})
// 初始化选择的用户
selectedUsers.value = Array.isArray(props.value) ? props.value : props.value.split(',')
})
</script>
<style scoped>
:deep(.arco-input-append) {
padding: 0;
.arco-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border: 1px solid transparent;
}
}
</style>

View File

@@ -0,0 +1,13 @@
export interface UserSelectPropType {
multiple: boolean
value: string | string[]
}
export interface UserItem {
id: string
nickname: string
deptName: string
roleNames: string
phone: string
email: string
status: number
}

View File

@@ -2,8 +2,8 @@
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
class="verify-img-panel"
:style="{
'width': setSize.imgWidth,
'height': setSize.imgHeight,
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
@@ -11,26 +11,26 @@
}"
>
<div
v-show="showRefresh"
class="verify-refresh"
style="z-index: 3"
@click="refresh"
v-show="showRefresh"
class="verify-refresh"
style="z-index: 3"
@click="refresh"
>
<i class="iconfont icon-refresh"></i>
</div>
<img
ref="canvas"
:src="`data:image/png;base64,${pointBackImgBase}`"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
ref="canvas"
:src="`data:image/png;base64,${pointBackImgBase}`"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
'color': '#fff',
'z-index': 9999,
@@ -50,8 +50,8 @@
</div>
<div
class="verify-bar-area"
:style="{
class="verify-bar-area"
:style="{
'width': setSize.imgWidth,
'color': barAreaColor,
'border-color': barAreaBorderColor,
@@ -70,11 +70,11 @@ import {
onMounted,
reactive,
ref,
toRefs
toRefs,
} from 'vue'
import {
checkBehaviorCaptcha,
getBehaviorCaptcha
getBehaviorCaptcha,
} from '@/apis/common/captcha'
import { resetSize } from '@/utils/verify'
import { encryptByAes } from '@/utils/encrypt'
@@ -85,34 +85,34 @@ export default {
// 弹出式pop固定fixed
mode: {
type: String,
default: ''
default: '',
},
captchaType: {
type: String
type: String,
},
// 间隔
vSpace: {
type: Number,
default: 5
default: 5,
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
height: '155px',
}
}
},
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
height: '40px',
}
}
}
},
},
},
setup(props) {
const { mode, captchaType } = toRefs(props)
@@ -129,7 +129,7 @@ export default {
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
barWidth: 0,
})
const tempPoints = reactive([])
const text = ref('')
@@ -141,7 +141,7 @@ export default {
// 请求背景图片和验证图片
function getPicture() {
const data = {
captchaType: captchaType.value
captchaType: captchaType.value,
}
getBehaviorCaptcha(data).then((res) => {
pointBackImgBase.value = res.data.originalImageBase64
@@ -226,7 +226,7 @@ export default {
const captchaVerification = secretKey.value
? encryptByAes(
`${backToken.value}---${JSON.stringify(checkPosArr)}`,
secretKey.value
secretKey.value,
)
: `${backToken.value}---${JSON.stringify(checkPosArr)}`
const data = {
@@ -234,7 +234,7 @@ export default {
pointJson: secretKey.value
? encryptByAes(JSON.stringify(checkPosArr), secretKey.value)
: JSON.stringify(checkPosArr),
token: backToken.value
token: backToken.value,
}
checkBehaviorCaptcha(data).then((res) => {
if (res.success && res.data.repCode === '0000') {
@@ -289,8 +289,8 @@ export default {
createPoint,
refresh,
getPicture,
pointTransform
pointTransform,
}
}
},
}
</script>

View File

@@ -1,38 +1,35 @@
<template>
<div style="position: relative">
<div
v-if="type === '2'"
class="verify-img-out"
:style="{ height: `${parseInt(setSize.imgHeight) + vSpace}px` }"
v-if="type === '2'"
class="verify-img-out"
:style="{ height: `${parseInt(setSize.imgHeight) + vSpace}px` }"
>
<div
class="verify-img-panel"
:style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
class="verify-img-panel"
:style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
>
<img
:src="`data:image/png;base64,${backImgBase}`"
alt=""
style="width: 100%; height: 100%; display: block"
:src="`data:image/png;base64,${backImgBase}`"
alt=""
style="width: 100%; height: 100%; display: block"
/>
<div v-show="showRefresh" class="verify-refresh" @click="refresh"
>
<i class="iconfont icon-refresh"></i
>
</div>
<div v-show="showRefresh" class="verify-refresh" @click="refresh">
<i class="iconfont icon-refresh"></i>
</div>
<transition name="tips">
<span
v-if="tipWords"
class="verify-tips"
:class="passFlag ? 'suc-bg' : 'err-bg'"
>{{ tipWords }}</span
>
v-if="tipWords"
class="verify-tips"
:class="passFlag ? 'suc-bg' : 'err-bg'"
>{{ tipWords }}</span>
</transition>
</div>
</div>
<!-- 公共部分 -->
<div
class="verify-bar-area"
:style="{
class="verify-bar-area"
:style="{
'width': setSize.imgWidth,
'height': barSize.height,
'line-height': barSize.height,
@@ -40,8 +37,8 @@
>
<span class="verify-msg" v-text="text"></span>
<div
class="verify-left-bar"
:style="{
class="verify-left-bar"
:style="{
'width': leftBarWidth !== undefined ? leftBarWidth : barSize.height,
'height': barSize.height,
'border-color': leftBarBorderColor,
@@ -50,25 +47,25 @@
>
<span class="verify-msg" v-text="finishText"></span>
<div
class="verify-move-block"
:style="{
class="verify-move-block"
:style="{
'width': barSize.height,
'height': barSize.height,
'background-color': moveBlockBackgroundColor,
'left': moveBlockLeft,
'transition': transitionLeft,
}"
@touchstart="start"
@mousedown="start"
@touchstart="start"
@mousedown="start"
>
<i
class="verify-icon iconfont" :class="[iconClass]"
:style="{ color: iconColor }"
class="verify-icon iconfont" :class="[iconClass]"
:style="{ color: iconColor }"
></i>
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
v-if="type === '2'"
class="verify-sub-block"
:style="{
'width':
`${Math.floor((parseInt(setSize.imgWidth) * 47) / 310)}px`,
'height': setSize.imgHeight,
@@ -77,9 +74,9 @@
}"
>
<img
:src="`data:image/png;base64,${blockBackImgBase}`"
alt=""
style="
:src="`data:image/png;base64,${blockBackImgBase}`"
alt=""
style="
width: 100%;
height: 100%;
display: block;
@@ -102,11 +99,11 @@ import {
reactive,
ref,
toRefs,
watch
watch,
} from 'vue'
import {
checkBehaviorCaptcha,
getBehaviorCaptcha
getBehaviorCaptcha,
} from '@/apis/common/captcha'
import { encryptByAes } from '@/utils/encrypt'
import { resetSize } from '@/utils/verify'
@@ -115,52 +112,52 @@ export default {
name: 'VerifySlide',
props: {
captchaType: {
type: String
type: String,
},
type: {
type: String,
default: '1'
default: '1',
},
// 弹出式pop固定fixed
mode: {
type: String,
default: 'fixed'
default: 'fixed',
},
vSpace: {
type: Number,
default: 5
default: 5,
},
explain: {
type: String,
default: '向右滑动完成验证'
default: '向右滑动完成验证',
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
height: '155px',
}
}
},
},
blockSize: {
type: Object,
default() {
return {
width: '50px',
height: '50px'
height: '50px',
}
}
},
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
height: '40px',
}
}
}
},
},
},
setup(props) {
const { mode, captchaType, type, blockSize, explain } = toRefs(props)
@@ -180,7 +177,7 @@ export default {
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
barWidth: 0,
})
const top = ref(0)
const left = ref(0)
@@ -201,7 +198,7 @@ export default {
// 请求背景图片和验证图片
function getPicture() {
const data = {
captchaType: captchaType.value
captchaType: captchaType.value,
}
getBehaviorCaptcha(data).then((res) => {
backImgBase.value = res.data.originalImageBase64
@@ -276,7 +273,7 @@ export default {
if (status.value && isEnd.value === false) {
let moveLeftDistance = Number.parseInt(
(moveBlockLeft.value || '').replace('px', ''),
10
10,
)
moveLeftDistance
= (moveLeftDistance * 310) / Number.parseInt(`${setSize.imgWidth}`, 10)
@@ -285,10 +282,10 @@ export default {
pointJson: secretKey.value
? encryptByAes(
JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
secretKey.value
secretKey.value,
)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value
token: backToken.value,
}
checkBehaviorCaptcha(data).then((res) => {
if (res.success && res.data.repCode === '0000') {
@@ -313,13 +310,13 @@ export default {
? encryptByAes(
`${backToken.value}---${JSON.stringify({
x: moveLeftDistance,
y: 5.0
y: 5.0,
})}`,
secretKey.value
secretKey.value,
)
: `${backToken.value}---${JSON.stringify({
x: moveLeftDistance,
y: 5.0
y: 5.0,
})}`
setTimeout(() => {
tipWords.value = ''
@@ -410,7 +407,7 @@ export default {
x = e.touches[0].pageX
}
startLeft.value = Math.floor(
x - barArea.value.getBoundingClientRect().left
x - barArea.value.getBoundingClientRect().left,
)
startMoveTime.value = +new Date() // 开始滑动的时间
if (isEnd.value === false) {
@@ -452,8 +449,8 @@ export default {
transitionWidth,
barArea,
refresh,
start
start,
}
}
},
}
</script>

View File

@@ -1,8 +1,8 @@
<template>
<div v-show="showBox" :class="mode === 'pop' ? 'mask' : ''">
<div
:class="mode === 'pop' ? 'verifybox' : ''"
:style="{ 'max-width': `${parseInt(imgSize.width) + 30}px` }"
:class="mode === 'pop' ? 'verifybox' : ''"
:style="{ 'max-width': `${parseInt(imgSize.width) + 30}px` }"
>
<div v-if="mode === 'pop'" class="verifybox-top">
请完成安全验证
@@ -11,24 +11,25 @@
</span>
</div>
<div
class="verifybox-bottom"
:style="{ padding: mode === 'pop' ? '15px' : '0' }"
class="verifybox-bottom"
:style="{ padding: mode === 'pop' ? '15px' : '0' }"
>
<!-- 验证码容器 -->
<!-- eslint-disable-next-line vue/no-restricted-v-bind -->
<component
:is="componentType"
v-if="componentType"
ref="instance"
:captcha-type="captchaType"
:type="verifyType"
:figure="figure"
:arith="arith"
:mode="mode"
:v-space="vSpace"
:explain="explain"
:img-size="imgSize"
:block-size="blockSize"
:bar-size="barSize"
:is="componentType"
v-if="componentType"
ref="instance"
:space="space"
:captcha-type="captchaType"
:type="verifyType"
:figure="figure"
:arith="arith"
:mode="mode"
:explain="explain"
:img-size="imgSize"
:block-size="blockSize"
:bar-size="barSize"
></component>
</div>
</div>
@@ -44,44 +45,44 @@ export default {
name: 'Vue2Verify',
components: {
VerifySlide,
VerifyPoints
VerifyPoints,
},
props: {
captchaType: {
type: String,
required: true
required: true,
},
figure: {
type: Number
type: Number,
},
arith: {
type: Number
type: Number,
},
mode: {
type: String,
default: 'pop'
default: 'pop',
},
vSpace: {
type: Number
space: {
type: Number,
},
explain: {
type: String
type: String,
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
height: '155px',
}
}
},
},
blockSize: {
type: Object
type: Object,
},
barSize: {
type: Object
}
type: Object,
},
},
setup(props) {
const { captchaType, mode } = toRefs(props)
@@ -135,9 +136,9 @@ export default {
instance,
showBox,
closeBox,
show
show,
}
}
},
}
</script>

View File

@@ -1,5 +1,5 @@
type LabelValueItem = { label: string, value: number, color: string }
interface LabelValueItem { label: string, value: number, color: string }
export const DisEnableStatusList: LabelValueItem[] = [
{ label: '启用', value: 1, color: 'green' },
{ label: '禁用', value: 2, color: 'red' }
{ label: '禁用', value: 2, color: 'red' },
]

View File

@@ -11,7 +11,7 @@ export const FileTypeList: FileTypeListItem[] = [
{ name: '文档', value: 3, icon: 'file-txt' },
{ name: '视频', value: 4, icon: 'file-video-color' },
{ name: '音频', value: 5, icon: 'file-music' },
{ name: '其他', value: 1, icon: 'file-other' }
{ name: '其他', value: 1, icon: 'file-other' },
]
export interface FileExtendNameIconMap {
@@ -34,7 +34,7 @@ export const FileIcon: FileExtendNameIconMap = {
html: 'file-html',
css: 'file-css',
js: 'file-js',
other: 'file-other' // 未知文件
other: 'file-other', // 未知文件
}
/** 图片类型 */

View File

@@ -6,5 +6,5 @@ export default {
install(Vue: App) {
Vue.directive('permission', hasPerm)
Vue.directive('role', hasRole)
}
},
}

View File

@@ -29,7 +29,7 @@ const directive: Directive = {
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding)
}
},
}
export default directive

View File

@@ -28,7 +28,7 @@ const directive: Directive = {
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkRole(el, binding)
}
},
}
export default directive

View File

@@ -3,7 +3,7 @@ import { useRoute, useRouter } from 'vue-router'
import { type FormInstance, Message, Modal } from '@arco-design/web-vue'
import { isEqual } from 'lodash-es'
type Option<T> = {
interface Option<T> {
key?: string
formRef?: Ref<FormInstance>
initApi: () => Promise<ApiRes<T>>
@@ -49,7 +49,7 @@ export function useFormCurd<T = any>(option: Option<T>) {
() => route.query,
() => {
initForm()
}
},
)
watch(
@@ -61,7 +61,7 @@ export function useFormCurd<T = any>(option: Option<T>) {
isChanged.value = true
}
},
{ immediate: true, deep: true }
{ immediate: true, deep: true },
)
const save = async () => {
@@ -92,7 +92,7 @@ export function useFormCurd<T = any>(option: Option<T>) {
hideCancel: false,
onOk: () => {
router.back()
}
},
})
} else {
router.back()

View File

@@ -6,5 +6,4 @@ export * from './modules/useTable'
export * from './modules/useForm'
export * from './modules/useDevice'
export * from './modules/useBreakpoint'
export * from './modules/useBreakpointIndex'
export * from './modules/useDownload'

View File

@@ -12,7 +12,7 @@ export function useBreakpoint() {
md: 768, // >=768
lg: 992, // >=992
xl: 1200, // >=1200
xxl: 1600 // >=1600
xxl: 1600, // >=1600
})
const arr = breakpoints.current() as ComputedRef<Breakpoint[]>

View File

@@ -1,24 +0,0 @@
import { useBreakpoint } from '@/hooks'
type BreakpointMap = {
xs: number
sm: number
md: number
lg: number
xl: number
xxl: number
}
export function useBreakpointIndex(callback: (v: number) => void, breakpointObj?: BreakpointMap) {
const { breakpoint } = useBreakpoint()
watch(
() => breakpoint.value,
(v) => {
const def = { xs: 0, sm: 0, md: 0, lg: 1, xl: 1, xxl: 2 }
const obj = breakpointObj || def
callback(obj[v as keyof typeof obj])
},
{ immediate: true }
)
}

View File

@@ -10,7 +10,7 @@ import { Message, Notification } from '@arco-design/web-vue'
interface NavigatorWithMsSaveOrOpenBlob extends Navigator {
msSaveOrOpenBlob: (blob: Blob, fileName: string) => void
}
export const useDownload = async (api: () => Promise<any>, isNotify = true, tempName = '', fileType = '.xlsx') => {
export const useDownload = async (api: () => Promise<any>, isNotify = false, tempName = '', fileType = '.xlsx') => {
try {
const res = await api()
if (res.headers['content-disposition']) {
@@ -21,7 +21,7 @@ export const useDownload = async (api: () => Promise<any>, isNotify = true, temp
if (isNotify && !res?.code) {
Notification.warning({
title: '温馨提示',
content: '如果数据庞大会导致下载缓慢哦,请您耐心等待!'
content: '如果数据庞大会导致下载缓慢哦,请您耐心等待!',
})
}
if (res.status !== 200 || res.data == null || !(res.data instanceof Blob)) {

View File

@@ -14,6 +14,6 @@ export function useLoading(initValue = false) {
return {
loading,
setLoading,
toggle
toggle,
}
}

View File

@@ -3,7 +3,7 @@ import { useBreakpoint } from '@/hooks'
type Callback = () => void
export type Options = {
export interface Options {
defaultPageSize: number
defaultSizeOptions: number[]
}
@@ -27,7 +27,7 @@ export function usePagination(callback: Callback, options: Options = { defaultPa
pagination.current = 1
pagination.pageSize = size
callback && callback()
}
},
})
watch(
@@ -36,7 +36,7 @@ export function usePagination(callback: Callback, options: Options = { defaultPa
pagination.simple = ['xs'].includes(breakpoint.value)
pagination.showTotal = !['xs'].includes(breakpoint.value)
},
{ immediate: true }
{ immediate: true },
)
const changeCurrent = pagination.onChange
@@ -54,6 +54,6 @@ export function usePagination(callback: Callback, options: Options = { defaultPa
pagination,
changeCurrent,
changePageSize,
setTotal
setTotal,
}
}

View File

@@ -5,7 +5,7 @@ import { useLoading } from '@/hooks'
export function useRequest<T>(
api: () => Promise<AxiosResponse<ApiRes<T>>>,
defaultValue = [] as unknown as T,
isLoading = true
isLoading = true,
) {
const { loading, setLoading } = useLoading(isLoading)
const response = ref<T>(defaultValue)

View File

@@ -11,7 +11,7 @@ interface Options<T, U> {
paginationOption?: paginationOptions
}
type PaginationParams = { page: number, size: number }
interface PaginationParams { page: number, size: number }
type Api<T> = (params: PaginationParams) => Promise<ApiRes<PageRes<T[]>>> | Promise<ApiRes<T[]>>
export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U>) {
@@ -60,7 +60,7 @@ export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U
// 删除
const handleDelete = async <T>(
deleteApi: () => Promise<ApiRes<T>>,
options?: { title?: string, content?: string, successTip?: string, showModal?: boolean }
options?: { title?: string, content?: string, successTip?: string, showModal?: boolean },
): Promise<boolean | undefined> => {
const onDelete = async () => {
try {
@@ -85,7 +85,7 @@ export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U
okButtonProps: { status: 'danger' },
hideCancel: false,
maskClosable: false,
onBeforeOk: onDelete
onBeforeOk: onDelete,
})
}

View File

@@ -1,7 +1,9 @@
<template>
<div class="layout-mix">
<section v-if="isDesktop" class="layout-mix-left" :class="{ 'app-menu-dark': appStore.menuDark }"
:style="appStore.menuDark ? appStore.themeCSSVar : undefined">
<section
v-if="isDesktop" class="layout-mix-left" :class="{ 'app-menu-dark': appStore.menuDark }"
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>
<Menu :menus="leftMenus" :menu-style="{ width: '220px', flex: 1 }"></Menu>
</section>
@@ -9,8 +11,10 @@
<section class="layout-mix-right">
<header class="header">
<MenuFoldBtn></MenuFoldBtn>
<a-menu v-if="isDesktop" mode="horizontal" :selected-keys="activeMenu" :auto-open-selected="false"
:trigger-props="{ animationName: 'slide-dynamic-origin' }" @menu-item-click="onMenuItemClick">
<a-menu
v-if="isDesktop" mode="horizontal" :selected-keys="activeMenu" :auto-open-selected="false"
:trigger-props="{ animationName: 'slide-dynamic-origin' }" @menu-item-click="onMenuItemClick"
>
<a-menu-item v-for="item in topMenus" :key="item.path">
<template #icon>
<GiSvgIcon :name="getMenuIcon(item)" :size="24" />
@@ -94,7 +98,7 @@ watch(
getLeftMenus(newPath)
})
},
{ immediate: true }
{ immediate: true },
)
</script>

View File

@@ -1,8 +1,12 @@
<template>
<div v-if="isDesktop" class="asider" :class="{ 'app-menu-dark': appStore.menuDark }"
:style="appStore.menuDark ? appStore.themeCSSVar : undefined">
<a-layout-sider class="menu" collapsible breakpoint="xl" hide-trigger :width="230"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse">
<div
v-if="isDesktop" class="asider" :class="{ 'app-menu-dark': appStore.menuDark }"
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<a-layout-sider
class="menu" collapsible breakpoint="xl" hide-trigger :width="230"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>
<a-scrollbar outer-class="menu-scroll-view" style="height: 100%; overflow: auto">
<Menu></Menu>

View File

@@ -30,7 +30,7 @@ const queryParam = reactive({
isRead: false,
sort: ['createTime,desc'],
page: 1,
size: 5
size: 5,
})
const messageList = ref<MessageResp[]>()

View File

@@ -6,16 +6,20 @@
<a-space>
<a-badge>
<template #content>
<icon-check-circle-fill v-if="appStore.layout === 'left'" style="color: rgb(var(--success-6))"
:size="16"></icon-check-circle-fill>
<icon-check-circle-fill
v-if="appStore.layout === 'left'" style="color: rgb(var(--success-6))"
:size="16"
></icon-check-circle-fill>
</template>
<LayoutItem mode="left" @click="appStore.layout = 'left'"></LayoutItem>
<p class="layout-text">默认布局</p>
</a-badge>
<a-badge>
<template #content>
<icon-check-circle-fill v-if="appStore.layout === 'mix'" :size="16"
style="color: rgb(var(--success-6))"></icon-check-circle-fill>
<icon-check-circle-fill
v-if="appStore.layout === 'mix'" :size="16"
style="color: rgb(var(--success-6))"
></icon-check-circle-fill>
</template>
<LayoutItem mode="mix" @click="appStore.layout = 'mix'"></LayoutItem>
<p class="layout-text">混合布局</p>
@@ -25,8 +29,10 @@
<a-divider orientation="center">系统主题</a-divider>
<a-row justify="center">
<ColorPicker theme="dark" :color="appStore.themeColor" :sucker-hide="true" :colors-default="defaultColorList"
@change-color="changeColor"></ColorPicker>
<ColorPicker
theme="dark" :color="appStore.themeColor" :sucker-hide="true" :colors-default="defaultColorList"
@change-color="changeColor"
></ColorPicker>
</a-row>
<a-divider orientation="center">界面显示</a-divider>
@@ -36,16 +42,20 @@
<a-switch v-model="appStore.tab" />
</a-descriptions-item>
<a-descriptions-item label="页签风格">
<a-select v-model="appStore.tabMode" placeholder="请选择" :options="tabModeList" :disabled="!appStore.tab"
:trigger-props="{ autoFitPopupMinWidth: true }" :style="{ width: '120px' }">
<a-select
v-model="appStore.tabMode" placeholder="请选择" :options="tabModeList" :disabled="!appStore.tab"
:trigger-props="{ autoFitPopupMinWidth: true }" :style="{ width: '120px' }"
>
</a-select>
</a-descriptions-item>
<a-descriptions-item label="动画显示">
<a-switch v-model="appStore.animate" />
</a-descriptions-item>
<a-descriptions-item label="动画显示">
<a-select v-model="appStore.animateMode" placeholder="请选择" :options="animateModeList"
:disabled="!appStore.animate" :style="{ width: '120px' }">
<a-select
v-model="appStore.animateMode" placeholder="请选择" :options="animateModeList"
:disabled="!appStore.animate" :style="{ width: '120px' }"
>
</a-select>
</a-descriptions-item>
<a-descriptions-item label="深色菜单">
@@ -72,17 +82,16 @@
import { ColorPicker } from 'vue-color-kit'
import 'vue-color-kit/dist/vue-color-kit.css'
import LayoutItem from './components/LayoutItem.vue'
import { useAppStore, useUserStore } from '@/stores'
import { useAppStore } from '@/stores'
defineOptions({ name: 'SettingDrawer' })
const appStore = useAppStore()
const userStore = useUserStore()
const visible = ref(false)
const tabModeList: App.TabItem[] = [
{ label: '卡片', value: 'card' },
{ label: '间隔卡片', value: 'card-gutter' },
{ label: '圆角', value: 'rounded' }
{ label: '圆角', value: 'rounded' },
]
const animateModeList: App.AnimateItem[] = [
@@ -90,14 +99,13 @@ const animateModeList: App.AnimateItem[] = [
{ label: '滑动', value: 'fade-slide' },
{ label: '渐变', value: 'fade' },
{ label: '底部滑出', value: 'fade-bottom' },
{ label: '缩放消退', value: 'fade-scale' }
{ label: '缩放消退', value: 'fade-scale' },
]
const open = () => {
visible.value = true
}
const waterMarkPlaceholder = ref<string>()
defineExpose({ open })
// 默认显示的主题色列表
@@ -117,10 +125,10 @@ const defaultColorList = [
'#43a047',
'#e53935',
'#f4511e',
'#6d4c41'
'#6d4c41',
]
type ColorObj = {
interface ColorObj {
hex: string
hsv: { h: number, s: number, v: number }
rgba: { r: number, g: number, b: number, a: number }

View File

@@ -48,10 +48,8 @@
<a-dropdown trigger="hover">
<a-row align="center" :wrap="false" class="user">
<!-- 管理员头像 -->
<a-avatar :size="32">
<img :src="userStore.avatar" alt="avatar" />
</a-avatar>
<span class="username">{{ userStore.name }}</span>
<Avatar :src="userStore.avatar" :name="userStore.nickname" :size="32" />
<span class="username">{{ userStore.nickname }}</span>
<icon-down />
</a-row>
<template #content>
@@ -142,7 +140,7 @@ const logout = () => {
} catch (error) {
return false
}
}
},
})
}

View File

@@ -10,7 +10,7 @@
import { useAppStore } from '@/stores'
const props = withDefaults(defineProps<Props>(), {
collapsed: false
collapsed: false,
})
const appStore = useAppStore()
const title = computed(() => appStore.getTitle())

View File

@@ -3,8 +3,8 @@
<a-menu-item
v-if="
isOneShowingChild
&& (!onlyOneChild?.children || onlyOneChild?.meta?.noShowingChildren)
&& !item?.meta?.alwaysShow
&& (!onlyOneChild?.children || onlyOneChild?.meta?.noShowingChildren)
&& !item?.meta?.alwaysShow
"
v-bind="attrs"
:key="onlyOneChild?.path"

View File

@@ -73,7 +73,7 @@ watch(
() => route.fullPath,
() => {
handleRouteChange()
}
},
)
// 点击页签

View File

@@ -1,2 +1,2 @@
/** 省市区数据类型 */
export type MockAreaItem = { label: string, code: string, children?: MockAreaItem[] }
export interface MockAreaItem { label: string, code: string, children?: MockAreaItem[] }

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ export const resultSuccess = (data: unknown) => {
code: 200,
data,
msg: '请求成功',
success: true
success: true,
})
}
@@ -17,7 +17,7 @@ export const resultError = (data: unknown, msg: string, code = 500) => {
code,
data,
msg,
success: false
success: false,
})
}

View File

@@ -19,6 +19,6 @@ export default defineMock([
const data = parent?.item?.children?.map((i) => ({ label: i.label, code: i.code }))
return resultSuccess(data)
}
}
}
},
},
])

View File

@@ -13,25 +13,25 @@ export const constantRoutes: RouteRecordRaw[] = [
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/default/redirect/index.vue')
}
]
component: () => import('@/views/default/redirect/index.vue'),
},
],
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
meta: { hidden: true },
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),
meta: { hidden: true }
meta: { hidden: true },
},
{
path: '/403',
component: () => import('@/views/default/error/403.vue'),
meta: { hidden: true }
meta: { hidden: true },
},
{
path: '/',
@@ -44,25 +44,25 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/dashboard/workplace',
name: 'Workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: { title: '工作台', icon: 'desktop', hidden: false, affix: true }
meta: { title: '工作台', icon: 'desktop', hidden: false, affix: true },
},
{
path: '/dashboard/analysis',
name: 'Analysis',
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: { title: '分析页', icon: 'insert-chart', hidden: false }
}
]
meta: { title: '分析页', icon: 'insert-chart', hidden: false },
},
],
},
{
path: '/social/callback',
component: () => import('@/views/login/social/index.vue'),
meta: { hidden: true }
meta: { hidden: true },
},
{
path: '/pwdExpired',
component: () => import('@/views/login/pwdExpired/index.vue'),
meta: { hidden: true }
meta: { hidden: true },
},
{
path: '/setting',
@@ -74,22 +74,22 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/setting/profile',
name: 'SettingProfile',
component: () => import('@/views/setting/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false }
meta: { title: '个人中心', showInTabs: false },
},
{
path: '/setting/message',
name: 'SettingMessage',
component: () => import('@/views/setting/message/index.vue'),
meta: { title: '消息中心', showInTabs: false }
}
]
}
meta: { title: '消息中心', showInTabs: false },
},
],
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes,
scrollBehavior: () => ({ left: 0, top: 0 })
scrollBehavior: () => ({ left: 0, top: 0 }),
})
/**

View File

@@ -27,7 +27,7 @@ const handleNotification = () => {
position: 'bottomRight',
footer: () => {
return h(Space, {}, () => [h(Button, { type: 'primary', onClick: () => onUpdateSystem(id) }, '更新'), h(Button, { type: 'secondary', onClick: () => onCloseUpdateSystem(id) }, '关闭')])
}
},
})
}
@@ -37,7 +37,7 @@ const handleNotification = () => {
*/
const getVersionTag = async () => {
const response = await fetch('/', {
cache: 'no-cache'
cache: 'no-cache',
})
return response.headers.get('etag') || response.headers.get('last-modified')
}

View File

@@ -59,7 +59,7 @@ const storeSetup = () => {
// 初始化系统配置
const initSiteConfig = () => {
listOptionDict({
category: 'SITE'
category: 'SITE',
}).then((res) => {
const resMap = new Map()
res.data.forEach((item) => {
@@ -118,7 +118,7 @@ const storeSetup = () => {
getLogo,
getTitle,
getCopyright,
getForRecord
getForRecord,
}
}

View File

@@ -9,7 +9,7 @@ const storeSetup = () => {
if (_code !== null && _code !== '') {
dictData.value.push({
code: _code,
items
items,
})
}
}
@@ -50,7 +50,7 @@ const storeSetup = () => {
setDict,
getDict,
deleteDict,
cleanDict
cleanDict,
}
}

View File

@@ -66,8 +66,8 @@ const formatAsyncRoutes = (menus: RouteItem[]) => {
keepAlive: item.isCache,
icon: item.icon,
showInTabs: item.showInTabs,
activeMenu: item.activeMenu
}
activeMenu: item.activeMenu,
},
}
})
return routes as RouteRecordRaw[]
@@ -124,7 +124,7 @@ const storeSetup = () => {
return {
routes,
asyncRoutes,
generateRoutes
generateRoutes,
}
}

View File

@@ -127,7 +127,7 @@ const storeSetup = () => {
closeRight,
closeAll,
reset,
init
init,
}
}

View File

@@ -11,11 +11,10 @@ import {
getUserInfo as getUserInfoApi,
logout as logoutApi,
phoneLogin as phoneLoginApi,
socialLogin as socialLoginApi
socialLogin as socialLoginApi,
} from '@/apis'
import { clearToken, getToken, setToken } from '@/utils/auth'
import { resetHasRouteFlag } from '@/router/permission'
import getAvatar from '@/utils/avatar'
const storeSetup = () => {
const userInfo = reactive<UserInfo>({
@@ -31,9 +30,10 @@ const storeSetup = () => {
registrationDate: '',
deptName: '',
roles: [],
permissions: []
permissions: [],
})
const name = computed(() => userInfo.nickname)
const nickname = computed(() => userInfo.nickname)
const username = computed(() => userInfo.username)
const avatar = computed(() => userInfo.avatar)
const token = ref(getToken() || '')
@@ -100,7 +100,7 @@ const storeSetup = () => {
const getInfo = async () => {
const res = await getUserInfoApi()
Object.assign(userInfo, res.data)
userInfo.avatar = getAvatar(res.data.avatar, res.data.gender)
userInfo.avatar = res.data.avatar
if (res.data.roles && res.data.roles.length) {
roles.value = res.data.roles
permissions.value = res.data.permissions
@@ -109,7 +109,8 @@ const storeSetup = () => {
return {
userInfo,
name,
nickname,
username,
avatar,
token,
roles,
@@ -122,10 +123,10 @@ const storeSetup = () => {
logout,
logoutCallBack,
getInfo,
resetToken
resetToken,
}
}
export const useUserStore = defineStore('user', storeSetup, {
persist: { paths: ['token', 'roles', 'permissions', 'pwdExpiredShow'], storage: localStorage }
persist: { paths: ['token', 'roles', 'permissions', 'pwdExpiredShow'], storage: localStorage },
})

View File

@@ -46,6 +46,7 @@ declare global {
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
@@ -66,10 +67,13 @@ declare global {
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']

View File

@@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
@@ -21,6 +22,7 @@ declare module 'vue' {
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
GiFlexibleBox: typeof import('./../components/GiFlexibleBox/index.vue')['default']
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
@@ -48,6 +50,8 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
UserSelectContent: typeof import('./../components/UserSelect/component/UserSelectContent.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']

View File

@@ -17,7 +17,7 @@ function getFileName(url: string) {
export function downloadByUrl({
url,
target = '_blank',
fileName
fileName,
}: {
url: string
target?: '_self' | '_blank'

View File

@@ -33,7 +33,7 @@ export function encryptByAes(word, keyWord = defaultKeyWork) {
const arcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(arcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
padding: CryptoJS.pad.Pkcs7,
})
return encrypted.toString()
}

View File

@@ -48,5 +48,5 @@ export default {
/** 验证用户是否含有指定角色,必须全部拥有 */
hasRoleAnd(roles: string[]) {
return roles.every((item) => authRole(item))
}
},
}

View File

@@ -30,12 +30,12 @@ const StatusCodeMessage: ICodeMessage = {
501: '服务未实现(501)',
502: '网络错误(502)',
503: '服务不可用(503)',
504: '网络超时(504)'
504: '网络超时(504)',
}
const http: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_PREFIX ?? import.meta.env.VITE_API_BASE_URL,
timeout: 30 * 1000
timeout: 30 * 1000,
})
// 请求拦截器
@@ -53,7 +53,7 @@ http.interceptors.request.use(
},
(error) => {
return Promise.reject(error)
}
},
)
// 响应拦截器
@@ -84,7 +84,7 @@ http.interceptors.response.use(
const userStore = useUserStore()
userStore.logoutCallBack()
router.replace('/login')
}
},
})
} else {
NProgress.done()
@@ -92,7 +92,7 @@ http.interceptors.response.use(
if (msg.length <= 15) {
messageErrorWrapper({
content: msg || '服务器端错误',
duration: 5 * 1000
duration: 5 * 1000,
})
} else {
notificationErrorWrapper(msg || '服务器端错误')
@@ -106,11 +106,10 @@ http.interceptors.response.use(
response
&& messageErrorWrapper({
content: StatusCodeMessage[response.status] || '服务器暂时未响应,请刷新页面并重试。若无法解决,请联系管理员',
duration: 5 * 1000
duration: 5 * 1000,
})
console.log(response.status)
return Promise.reject(error)
}
},
)
const request = <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> => {
@@ -139,7 +138,7 @@ const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig)
paramsSerializer: (obj) => {
return qs.stringify(obj)
},
...config
...config,
})
}
@@ -148,7 +147,7 @@ const post = <T = any>(url: string, params?: object, config?: AxiosRequestConfig
method: 'post',
url,
data: params,
...config
...config,
})
}
@@ -157,7 +156,7 @@ const put = <T = any>(url: string, params?: object, config?: AxiosRequestConfig)
method: 'put',
url,
data: params,
...config
...config,
})
}
@@ -166,7 +165,7 @@ const patch = <T = any>(url: string, params?: object, config?: AxiosRequestConfi
method: 'patch',
url,
data: params,
...config
...config,
})
}
@@ -175,7 +174,7 @@ const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig)
method: 'delete',
url,
data: params,
...config
...config,
})
}
const download = (url: string, params?: object, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
@@ -187,7 +186,7 @@ const download = (url: string, params?: object, config?: AxiosRequestConfig): Pr
paramsSerializer: (obj) => {
return qs.stringify(obj)
},
...config
...config,
})
}
export default { get, post, put, patch, del, request, requestNative, download }

View File

@@ -297,7 +297,7 @@ export function dateFormat(date = new Date(), pattern = YMD_HMS) {
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
'S+': date.getMilliseconds()
'S+': date.getMilliseconds(),
}
let formattedDate = pattern // Start with the pattern
@@ -341,7 +341,7 @@ export function parseCron(cron: string) {
try {
const parse = expressionNoYear(cron)
const iter = CronParser.parseExpression(parse, {
currentDate: dateFormat(new Date())
currentDate: dateFormat(new Date()),
})
const result: string[] = []
for (let i = 1; i <= 5; i++) {

View File

@@ -1,6 +1,6 @@
import mitt from 'mitt'
type Events = {
interface Events {
// 自定义事件名称
event: void
// 任意传递的参数

View File

@@ -19,13 +19,14 @@ export const Code_4 = /^\d{4}$/
/** @desc 正则-url链接 */
export const Url
= /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
// eslint-disable-next-line regexp/no-useless-quantifier
= /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-]*)?\??[-+=&;%@.\w]*#?\w*)?)$/
/** @desc 正则-16进颜色值 #333 #8c8c8c */
export const ColorRegex = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
export const ColorRegex = /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i
/** @desc 正则-只能是中文 */
export const OnlyCh = /^[\u4E00-\u9FA5]+$/gi
export const OnlyCh = /^[\u4E00-\u9FA5]+$/g
/** @desc 正则-只能是英文 */
export const OnlyEn = /^[a-zA-Z]*$/
export const OnlyEn = /^[a-z]*$/i

View File

@@ -34,6 +34,6 @@ export function resetSize(vm) {
imgWidth: img_width,
imgHeight: img_height,
barWidth: bar_width,
barHeight: bar_height
barHeight: bar_height,
}
}

View File

@@ -10,6 +10,7 @@
import { type EChartsOption, graphic } from 'echarts'
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisTimeslot as getData } from '@/apis/common'
import handleIcon from '@/assets/icons/slider.svg'
// 提示框
const tooltipItemsHtmlString = (items) => {
@@ -23,7 +24,7 @@ const tooltipItemsHtmlString = (items) => {
<span class="tooltip-value">
${el.value}
</span>
</div>`
</div>`,
)
.join('')
}
@@ -36,7 +37,7 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
left: '40',
right: 0,
top: '20',
bottom: '100'
bottom: '100',
},
xAxis: {
type: 'category',
@@ -49,24 +50,24 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
},
axisLine: {
lineStyle: {
color: isDark ? '#3f3f3f' : '#A9AEB8'
}
color: isDark ? '#3f3f3f' : '#A9AEB8',
},
},
axisTick: {
show: true,
alignWithLabel: true,
lineStyle: {
color: '#86909C'
color: '#86909C',
},
interval(idx: number) {
if (idx === 0) return false
if (idx === xAxis.value.length - 1) return false
return true
}
},
},
splitLine: {
show: true,
@@ -75,16 +76,16 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
return idx !== xAxis.value.length - 1
},
lineStyle: {
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
axisPointer: {
show: true,
lineStyle: {
color: '#23ADFF',
width: 2
}
}
width: 2,
},
},
},
yAxis: {
type: 'value',
@@ -95,17 +96,17 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
return `${value / 1000}k`
}
return `${value}`
}
},
},
axisLine: {
show: false
show: false,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
}
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
},
tooltip: {
show: true,
@@ -117,7 +118,7 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
${tooltipItemsHtmlString(params)}
</div>`
},
className: 'echarts-tooltip-diy'
className: 'echarts-tooltip-diy',
},
series: [
{
@@ -133,23 +134,23 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E0E3FF'
}
borderColor: '#E0E3FF',
},
},
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(17, 126, 255, 0.16)'
color: 'rgba(17, 126, 255, 0.16)',
},
{
offset: 1,
color: 'rgba(17, 128, 255, 0)'
}
])
}
}
color: 'rgba(17, 128, 255, 0)',
},
]),
},
},
],
dataZoom: [
{
@@ -159,23 +160,22 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
right: 14,
height: 14,
borderColor: 'transparent',
handleIcon:
'image://http://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/1ee5a8c6142b2bcf47d2a9f084096447.svg~tplv-49unhts6dw-image.image',
handleIcon: `image://${handleIcon}`,
handleSize: '20',
handleStyle: {
shadowColor: 'rgba(0, 0, 0, 0.2)',
shadowBlur: 4
shadowBlur: 4,
},
brushSelect: false,
backgroundColor: isDark ? '#313132' : '#F2F3F5'
backgroundColor: isDark ? '#313132' : '#F2F3F5',
},
{
type: 'inside',
start: 0,
end: 100,
zoomOnMouseWheel: false
}
]
zoomOnMouseWheel: false,
},
],
}
})

View File

@@ -29,7 +29,7 @@ const tooltipItemsHtmlString = (items) => {
<span class="tooltip-value">
${el.value}
</span>
</div>`
</div>`,
)
.join('')
}
@@ -43,14 +43,14 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
left: '38',
right: '5',
top: '10',
bottom: '50'
bottom: '50',
},
legend: {
bottom: -3,
icon: 'circle',
textStyle: {
color: '#4E5969'
}
color: '#4E5969',
},
},
xAxis: {
type: 'category',
@@ -63,13 +63,13 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
},
axisLine: {
show: false
show: false,
},
axisTick: {
show: false
show: false,
},
splitLine: {
show: true,
@@ -78,16 +78,16 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
return idx !== xAxis.value.length - 1
},
lineStyle: {
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
axisPointer: {
show: true,
lineStyle: {
color: '#23ADFF',
width: 2
}
}
width: 2,
},
},
},
yAxis: {
type: 'value',
@@ -98,17 +98,17 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
return `${value / 1000}k`
}
return `${value}`
}
},
},
axisLine: {
show: false
show: false,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
}
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
},
tooltip: {
show: true,
@@ -120,7 +120,7 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
${tooltipItemsHtmlString(params)}
</div>`
},
className: 'echarts-tooltip-diy'
className: 'echarts-tooltip-diy',
},
series: [
{
@@ -136,22 +136,22 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E0E3FF'
}
borderColor: '#E0E3FF',
},
},
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(17, 126, 255, 0.16)'
color: 'rgba(17, 126, 255, 0.16)',
},
{
offset: 1,
color: 'rgba(17, 128, 255, 0)'
}
])
}
color: 'rgba(17, 128, 255, 0)',
},
]),
},
},
{
name: '独立IP',
@@ -166,24 +166,24 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E2F2FF'
}
borderColor: '#E2F2FF',
},
},
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(17, 126, 255, 0.16)'
color: 'rgba(17, 126, 255, 0.16)',
},
{
offset: 1,
color: 'rgba(17, 128, 255, 0)'
}
])
}
}
]
color: 'rgba(17, 128, 255, 0)',
},
]),
},
},
],
}
})

View File

@@ -23,15 +23,15 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
icon: 'circle',
itemWidth: 8,
textStyle: {
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969'
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969',
},
itemStyle: {
borderWidth: 0
}
borderWidth: 0,
},
},
tooltip: {
show: true,
trigger: 'item'
trigger: 'item',
},
series: [
{
@@ -40,15 +40,15 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
center: ['50%', '42%'],
label: {
formatter: '{d}% ',
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
},
itemStyle: {
borderColor: isDark ? '#000' : '#fff',
borderWidth: 1
borderWidth: 1,
},
data: chartData.value
}
]
data: chartData.value,
},
],
}
})
@@ -64,8 +64,8 @@ const getChartData = async () => {
chartData.value.push({
...item,
itemStyle: {
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index]
}
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index],
},
})
})
} finally {

View File

@@ -39,7 +39,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
@@ -50,24 +49,24 @@ const count = ref(0)
const growth = ref(0)
const xAxis = ref<string[]>([])
const chartData = ref<number[]>([])
const { chartOption } = useChart((isDark: EChartsOption) => {
const { chartOption } = useChart(() => {
return {
grid: {
left: 0,
right: 30,
top: 10,
bottom: 0
bottom: 0,
},
xAxis: {
type: 'category',
data: xAxis.value
data: xAxis.value,
},
yAxis: {
show: false
show: false,
},
tooltip: {
show: true,
trigger: 'axis'
trigger: 'axis',
},
series: [
{
@@ -79,10 +78,10 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
lineStyle: {
color: '#246EFF',
width: 2,
type: 'dashed'
}
}
]
type: 'dashed',
},
},
],
}
})

View File

@@ -39,7 +39,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
@@ -49,13 +48,13 @@ const isDark = computed(() => appStore.theme === 'dark')
const count = ref(0)
const growth = ref(0)
const chartData = ref<number[]>([])
const { chartOption } = useChart((isDark: EChartsOption) => {
const { chartOption } = useChart(() => {
return {
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0
bottom: 0,
},
legend: {
show: true,
@@ -66,11 +65,11 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
itemWidth: 6,
itemHeight: 6,
textStyle: {
color: '#4E5969'
}
color: '#4E5969',
},
},
tooltip: {
show: true
show: true,
},
series: [
{
@@ -79,11 +78,11 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
radius: ['50%', '70%'],
center: ['30%', '50%'],
label: {
show: false
show: false,
},
data: chartData.value
}
]
data: chartData.value,
},
],
}
})
@@ -101,8 +100,8 @@ const getChartData = async () => {
name: `示例${index + 1}`,
value: item,
itemStyle: {
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index]
}
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index],
},
})
})
} finally {

View File

@@ -39,7 +39,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
import { type DashboardChartCommonResp, getDashboardOverviewIp as getData } from '@/apis'
@@ -52,24 +51,24 @@ const today = ref(0)
const growth = ref(0)
const xAxis = ref<string[]>([])
const chartData = ref<number[]>([])
const { chartOption } = useChart((isDark: EChartsOption) => {
const { chartOption } = useChart(() => {
return {
grid: {
left: 0,
right: 30,
top: 10,
bottom: 0
bottom: 0,
},
xAxis: {
type: 'category',
data: xAxis.value
data: xAxis.value,
},
yAxis: {
show: false
show: false,
},
tooltip: {
show: true,
trigger: 'axis'
trigger: 'axis',
},
series: [
{
@@ -79,10 +78,10 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
showSymbol: false,
lineStyle: {
color: '#2CAB40',
width: 2
}
}
]
width: 2,
},
},
],
}
})

View File

@@ -39,7 +39,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
import { type DashboardChartCommonResp, getDashboardOverviewPv as getData } from '@/apis'
@@ -52,24 +51,24 @@ const today = ref(0)
const growth = ref(0)
const xAxis = ref<string[]>([])
const chartData = ref<number[]>([])
const { chartOption } = useChart((isDark: EChartsOption) => {
const { chartOption } = useChart(() => {
return {
grid: {
left: 0,
right: 30,
top: 10,
bottom: 0
bottom: 0,
},
xAxis: {
type: 'category',
data: xAxis.value
data: xAxis.value,
},
yAxis: {
show: false
show: false,
},
tooltip: {
show: true,
trigger: 'axis'
trigger: 'axis',
},
series: [
{
@@ -79,10 +78,10 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
showSymbol: false,
lineStyle: {
color: '#246EFF',
width: 2
}
}
]
width: 2,
},
},
],
}
})

View File

@@ -21,7 +21,7 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
left: 55,
right: 20,
top: 0,
bottom: 20
bottom: 20,
},
xAxis: {
type: 'value',
@@ -30,38 +30,38 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
formatter(value: number, idx: number) {
if (idx === 0) return String(value)
return `${Number(value) / 1000}k`
}
},
},
splitLine: {
lineStyle: {
color: isDark ? '#484849' : '#E5E8EF'
}
}
color: isDark ? '#484849' : '#E5E8EF',
},
},
},
yAxis: {
type: 'category',
data: yAxis.value,
axisLabel: {
show: true,
color: '#4E5969'
color: '#4E5969',
},
axisTick: {
show: true,
length: 2,
lineStyle: {
color: '#A9AEB8'
color: '#A9AEB8',
},
alignWithLabel: true
alignWithLabel: true,
},
axisLine: {
lineStyle: {
color: isDark ? '#484849' : '#A9AEB8'
}
}
color: isDark ? '#484849' : '#A9AEB8',
},
},
},
tooltip: {
show: true,
trigger: 'axis'
trigger: 'axis',
},
series: [
{
@@ -70,10 +70,10 @@ const { chartOption } = useChart((isDark: EChartsOption) => {
barWidth: 7,
itemStyle: {
color: '#4086FF',
borderRadius: 4
}
}
]
borderRadius: 4,
},
},
],
}
})

Some files were not shown because too many files have changed in this diff Show More