mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-11-11 12:57:10 +08:00
Merge branch 'dev' of https://gitee.com/continew/continew-admin-ui into dev
This commit is contained in:
82
src/components/Avatar/index.vue
Normal file
82
src/components/Avatar/index.vue
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
defineOptions({ name: 'GiCellGender' })
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gender: 1
|
||||
gender: 1,
|
||||
})
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
defineOptions({ name: 'GiCellStatus' })
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
status: 1
|
||||
status: 1,
|
||||
})
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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: '' }
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
defineOptions({ name: 'GiCellTags' })
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: () => []
|
||||
data: () => [],
|
||||
})
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
127
src/components/GiEditTable/GiEditTable.vue
Normal file
127
src/components/GiEditTable/GiEditTable.vue
Normal 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>
|
||||
5
src/components/GiEditTable/index.ts
Normal file
5
src/components/GiEditTable/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import GiEditTable from './GiEditTable.vue'
|
||||
|
||||
export type * from './type'
|
||||
|
||||
export default GiEditTable
|
||||
51
src/components/GiEditTable/type.ts
Normal file
51
src/components/GiEditTable/type.ts
Normal 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)
|
||||
@@ -11,7 +11,7 @@ defineOptions({ name: 'GiFlexibleBox' })
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
direction: 'right'
|
||||
direction: 'right',
|
||||
})
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -43,6 +43,6 @@ export function useGiForm(initValue: Columns) {
|
||||
/** 设置 columns 某个对象属性的值 */
|
||||
setValue,
|
||||
/** 设置 columns.props 某个属性的值 */
|
||||
setPropsValue
|
||||
setPropsValue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ defineOptions({ name: 'GiIconSelector' })
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
enableCopy: false
|
||||
enableCopy: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select', 'update:modelValue'])
|
||||
|
||||
@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
icon: '',
|
||||
label: '',
|
||||
more: false,
|
||||
active: false
|
||||
active: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
defineOptions({ name: 'GiOverFlowTags' })
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: () => []
|
||||
data: () => [],
|
||||
})
|
||||
interface Props {
|
||||
data: string[]
|
||||
|
||||
@@ -15,7 +15,7 @@ defineOptions({ name: 'GiSvgIcon' })
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
name: '',
|
||||
color: '',
|
||||
size: 20
|
||||
size: 20,
|
||||
})
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ const isDark = useDark({
|
||||
storageKey: 'arco-theme',
|
||||
onChanged(dark: boolean) {
|
||||
appStore.toggleTheme(dark)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const toggleTheme = useToggle(isDark)
|
||||
|
||||
238
src/components/UserSelect/component/UserSelectContent.vue
Normal file
238
src/components/UserSelect/component/UserSelectContent.vue
Normal 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>
|
||||
110
src/components/UserSelect/index.vue
Normal file
110
src/components/UserSelect/index.vue
Normal 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>
|
||||
13
src/components/UserSelect/type.ts
Normal file
13
src/components/UserSelect/type.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user