mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-12 12:13:51 +08:00
发布代码生成、更新20+表单组件,优化数据字典,gf版本更新到2.3.1
This commit is contained in:
84
web/src/components/DatePicker/datePicker.vue
Normal file
84
web/src/components/DatePicker/datePicker.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<n-date-picker v-bind="$props" v-model:value="modelValue" :shortcuts="shortcuts" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
||||
import {
|
||||
dateToTimestamp,
|
||||
formatToDate,
|
||||
formatToDateTime,
|
||||
timestampToTime,
|
||||
defShortcuts,
|
||||
defRangeShortcuts,
|
||||
} from '@/utils/dateUtil';
|
||||
import { basicProps } from './props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: ['update:formValue', 'update:startValue', 'update:endValue'],
|
||||
setup(props, { emit }) {
|
||||
const shortcuts = ref<any>({});
|
||||
|
||||
function getTimestamp(value) {
|
||||
let t = dateToTimestamp(value);
|
||||
if (t === 0) {
|
||||
return new Date().getTime();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function setTimestamp(value) {
|
||||
if (!isTimeType()) {
|
||||
return formatToDate(new Date(Number(value)).toDateString());
|
||||
} else {
|
||||
return formatToDateTime(timestampToTime(Number(value / 1000)));
|
||||
}
|
||||
}
|
||||
|
||||
function isRangeType() {
|
||||
return props.type.indexOf('range') != -1;
|
||||
}
|
||||
|
||||
function isTimeType() {
|
||||
return props.type.indexOf('time') != -1;
|
||||
}
|
||||
|
||||
const modelValue = computed({
|
||||
get() {
|
||||
if (!isRangeType()) {
|
||||
return getTimestamp(props.formValue);
|
||||
} else {
|
||||
return [getTimestamp(props.startValue), getTimestamp(props.endValue)];
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
if (!isRangeType()) {
|
||||
emit('update:formValue', setTimestamp(value));
|
||||
} else {
|
||||
emit('update:startValue', setTimestamp(value[0]));
|
||||
emit('update:endValue', setTimestamp(value[1]));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isRangeType()) {
|
||||
shortcuts.value = defShortcuts();
|
||||
} else {
|
||||
shortcuts.value = defRangeShortcuts();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
modelValue,
|
||||
shortcuts,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
18
web/src/components/DatePicker/props.ts
Normal file
18
web/src/components/DatePicker/props.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { NDatePicker } from 'naive-ui';
|
||||
|
||||
export const basicProps = {
|
||||
...NDatePicker.props,
|
||||
formValue: {
|
||||
type: String as PropType<string> | undefined | Date,
|
||||
default: () => '',
|
||||
},
|
||||
startValue: {
|
||||
type: String as PropType<string> | undefined | Date,
|
||||
default: () => '',
|
||||
},
|
||||
endValue: {
|
||||
type: String as PropType<string> | undefined | Date,
|
||||
default: () => '',
|
||||
},
|
||||
};
|
||||
84
web/src/components/Editor/editor.vue
Normal file
84
web/src/components/Editor/editor.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<QuillEditor
|
||||
ref="quillEditor"
|
||||
:options="options"
|
||||
v-model:content="content"
|
||||
@ready="readyQuill"
|
||||
class="quillEditor"
|
||||
:id="quillEditorId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { QuillEditor } from '@vueup/vue-quill';
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import { getRandomString } from '@/utils/charset';
|
||||
export interface Props {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:value']);
|
||||
const quillEditorId = ref('quillEditorId-' + getRandomString(16, true));
|
||||
const quillEditor = ref();
|
||||
const content = ref();
|
||||
const props = withDefaults(defineProps<Props>(), { value: '' });
|
||||
const options = ref({
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
||||
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
||||
[{ direction: 'rtl' }], // text direction
|
||||
|
||||
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
[{ font: [] }],
|
||||
[{ align: [] }],
|
||||
['clean'],
|
||||
['image'],
|
||||
],
|
||||
},
|
||||
theme: 'snow',
|
||||
placeholder: '输入您要编辑的内容!',
|
||||
});
|
||||
|
||||
function readyQuill() {
|
||||
quillEditor.value.setHTML(props.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => content.value,
|
||||
(_newValue, _oldValue) => {
|
||||
if (quillEditor.value !== undefined) {
|
||||
emit('update:value', quillEditor.value.getHTML());
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true, // 深度监听
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
// 兼容表单分组 n-form-item-blank
|
||||
let dom = document.getElementById(quillEditorId.value);
|
||||
if (dom && dom.parentNode) {
|
||||
const parent = dom.parentNode as Element;
|
||||
if ('n-form-item-blank' === parent.className) {
|
||||
parent.setAttribute('style', 'display: block;');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ql-container {
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@ export interface FormSchema {
|
||||
labelMessageStyle?: object | string;
|
||||
defaultValue?: any;
|
||||
component?: ComponentType;
|
||||
componentProps?: object;
|
||||
componentProps?: object | any;
|
||||
slot?: string;
|
||||
rules?: object | object[];
|
||||
giProps?: GridItemProps;
|
||||
|
||||
94
web/src/components/IconSelector/AntdSelector.vue
Normal file
94
web/src/components/IconSelector/AntdSelector.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<n-popover trigger="click" placement="bottom" width="400">
|
||||
<template #trigger>
|
||||
<n-button>
|
||||
<template #icon>
|
||||
<n-icon size="20">
|
||||
<component :is="formValue !== '' ? formValue : 'AntDesignOutlined'" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-scrollbar class="grid-wrapper">
|
||||
<n-grid :cols="8" :collapsed="false" responsive="screen" style="height: 300px">
|
||||
<n-grid-item v-for="(item, index) of icons" :key="index">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-3 icon-wrapper"
|
||||
@click="onIconClick(item)"
|
||||
>
|
||||
<n-icon size="20">
|
||||
<component :is="item" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-scrollbar>
|
||||
<div class="flex justify-end mt-2 mb-2">
|
||||
<n-pagination
|
||||
:page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-slot="8"
|
||||
:item-count="itemCount"
|
||||
@update-page="onUpdatePage"
|
||||
/>
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, shallowReactive } from 'vue';
|
||||
import * as AntdIcons from '@vicons/antd';
|
||||
export default defineComponent({
|
||||
name: 'AntdSelector',
|
||||
components: AntdIcons,
|
||||
props: {
|
||||
value: String,
|
||||
option: String,
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { emit }) {
|
||||
const formValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value);
|
||||
},
|
||||
});
|
||||
|
||||
const iconArray = Object.keys(AntdIcons);
|
||||
const pageSize = 40;
|
||||
const icons = shallowReactive(iconArray.slice(0, 40));
|
||||
const currentPage = ref(1);
|
||||
const itemCount = computed(() => iconArray.length);
|
||||
|
||||
function onUpdatePage(page: number) {
|
||||
currentPage.value = page;
|
||||
icons.length = 0;
|
||||
const start = (currentPage.value - 1) * pageSize;
|
||||
icons.push(...iconArray.slice(start, start + pageSize));
|
||||
}
|
||||
|
||||
function onIconClick(item: any) {
|
||||
formValue.value = item;
|
||||
}
|
||||
return {
|
||||
icons,
|
||||
currentPage,
|
||||
pageSize,
|
||||
itemCount,
|
||||
onUpdatePage,
|
||||
onIconClick,
|
||||
formValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.grid-wrapper {
|
||||
.icon-wrapper {
|
||||
cursor: pointer;
|
||||
border: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
web/src/components/IconSelector/Ionicons5Selector.vue
Normal file
94
web/src/components/IconSelector/Ionicons5Selector.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<n-popover trigger="click" placement="bottom" width="400">
|
||||
<template #trigger>
|
||||
<n-button>
|
||||
<template #icon>
|
||||
<n-icon size="20">
|
||||
<component :is="formValue !== '' ? formValue : 'LogoIonic'" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-scrollbar class="grid-wrapper">
|
||||
<n-grid :cols="8" :collapsed="false" responsive="screen" style="height: 300px">
|
||||
<n-grid-item v-for="(item, index) of icons" :key="index">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-3 icon-wrapper"
|
||||
@click="onIconClick(item)"
|
||||
>
|
||||
<n-icon size="20">
|
||||
<component :is="item" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-scrollbar>
|
||||
<div class="flex justify-end mt-2 mb-2">
|
||||
<n-pagination
|
||||
:page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-slot="8"
|
||||
:item-count="itemCount"
|
||||
@update-page="onUpdatePage"
|
||||
/>
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, shallowReactive } from 'vue';
|
||||
import * as Ionicons5Icons from '@vicons/ionicons5';
|
||||
export default defineComponent({
|
||||
name: 'Ionicons5Selector',
|
||||
components: Ionicons5Icons,
|
||||
props: {
|
||||
value: String,
|
||||
option: String,
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { emit }) {
|
||||
const formValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value);
|
||||
},
|
||||
});
|
||||
|
||||
const iconArray = Object.keys(Ionicons5Icons);
|
||||
const pageSize = 40;
|
||||
const icons = shallowReactive(iconArray.slice(0, 40));
|
||||
const currentPage = ref(1);
|
||||
const itemCount = computed(() => iconArray.length);
|
||||
|
||||
function onUpdatePage(page: number) {
|
||||
currentPage.value = page;
|
||||
icons.length = 0;
|
||||
const start = (currentPage.value - 1) * pageSize;
|
||||
icons.push(...iconArray.slice(start, start + pageSize));
|
||||
}
|
||||
|
||||
function onIconClick(item: any) {
|
||||
formValue.value = item;
|
||||
}
|
||||
return {
|
||||
icons,
|
||||
currentPage,
|
||||
pageSize,
|
||||
itemCount,
|
||||
onUpdatePage,
|
||||
onIconClick,
|
||||
formValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.grid-wrapper {
|
||||
.icon-wrapper {
|
||||
cursor: pointer;
|
||||
border: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
web/src/components/IconSelector/index.vue
Normal file
44
web/src/components/IconSelector/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-input-group>
|
||||
<n-input v-bind="$props" :value="formValue" :style="{ width: '70%' }" />
|
||||
<template v-if="option === 'ionicons5'">
|
||||
<Ionicons5Selector v-model:value="formValue" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<AntdSelector v-model:value="formValue" />
|
||||
</template>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { basicProps } from '@/components/IconSelector/props';
|
||||
import Ionicons5Selector from '@/components/IconSelector/Ionicons5Selector.vue';
|
||||
import AntdSelector from '@/components/IconSelector/AntdSelector.vue';
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
components: { Ionicons5Selector, AntdSelector },
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { emit }) {
|
||||
const formValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
formValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
14
web/src/components/IconSelector/props.ts
Normal file
14
web/src/components/IconSelector/props.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { NInput } from 'naive-ui';
|
||||
|
||||
export const basicProps = {
|
||||
...NInput.props,
|
||||
option: {
|
||||
type: String as PropType<string>,
|
||||
default: 'antd', // ionicons5 | antd
|
||||
},
|
||||
value: {
|
||||
type: String as PropType<string>,
|
||||
default: () => '',
|
||||
},
|
||||
};
|
||||
@@ -18,7 +18,7 @@
|
||||
<slot name="tableTitle"></slot>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center table-toolbar-right">
|
||||
<div class="flex items-center table-toolbar-right" v-show="showTopRight">
|
||||
<!--顶部右侧区域-->
|
||||
<slot name="toolbar"></slot>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</n-tooltip>
|
||||
|
||||
<!--表格设置单独抽离成组件-->
|
||||
<ColumnSetting />
|
||||
<ColumnSetting :openChecked="openChecked" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-table">
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<div class="table-toolbar-inner-popover-title">
|
||||
<n-space>
|
||||
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll"
|
||||
>列展示
|
||||
</n-checkbox>
|
||||
>列展示</n-checkbox
|
||||
>
|
||||
<n-checkbox v-model:checked="selection" @update:checked="onSelection"
|
||||
>勾选列
|
||||
</n-checkbox>
|
||||
>勾选列</n-checkbox
|
||||
>
|
||||
<n-button text type="info" size="small" class="mt-1" @click="resetColumns"
|
||||
>重置
|
||||
</n-button>
|
||||
>重置</n-button
|
||||
>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,24 +92,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRaw, toRefs, unref, watchEffect } from 'vue';
|
||||
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
|
||||
import { useTableContext } from '../../hooks/useTableContext';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
DragOutlined,
|
||||
SettingOutlined,
|
||||
VerticalLeftOutlined,
|
||||
DragOutlined,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
} from '@vicons/antd';
|
||||
import Draggable from 'vuedraggable/src/vuedraggable';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
|
||||
interface Options {
|
||||
title: string;
|
||||
key: string;
|
||||
fixed?: boolean | 'left' | 'right';
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ColumnSetting',
|
||||
components: {
|
||||
@@ -119,30 +117,32 @@
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
},
|
||||
setup() {
|
||||
props: {
|
||||
openChecked: {
|
||||
type: Boolean as PropType<Boolean>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
const table: any = useTableContext();
|
||||
const columnsList = ref<Options[]>([]);
|
||||
const cacheColumnsList = ref<Options[]>([]);
|
||||
|
||||
const state = reactive({
|
||||
selection: true,
|
||||
selection: false,
|
||||
checkAll: true,
|
||||
checkList: [],
|
||||
defaultCheckList: [],
|
||||
});
|
||||
|
||||
const getSelection = computed(() => {
|
||||
return state.selection;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const columns = table.getColumns();
|
||||
if (columns.length) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
//初始化
|
||||
function init() {
|
||||
const columns: any[] = getColumns();
|
||||
@@ -153,23 +153,27 @@
|
||||
if (!columnsList.value.length) {
|
||||
columnsList.value = cloneDeep(newColumns);
|
||||
cacheColumnsList.value = cloneDeep(newColumns);
|
||||
// 只有首次加载时需要执行,重复执行会导致生成多个选项
|
||||
if (props.openChecked) {
|
||||
state.selection = true;
|
||||
if (newColumns[0].type != 'selection' && newColumns[0].key != 'selection') {
|
||||
onSelection(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
onSelection(true);
|
||||
}
|
||||
|
||||
//切换
|
||||
function onChange(checkList) {
|
||||
console.log('checkList:' + JSON.stringify(checkList));
|
||||
if (state.selection) {
|
||||
checkList.unshift('selection');
|
||||
}
|
||||
setColumns(checkList);
|
||||
}
|
||||
|
||||
//设置
|
||||
function setColumns(columns) {
|
||||
table.setColumns(columns);
|
||||
}
|
||||
|
||||
//获取
|
||||
function getColumns() {
|
||||
let newRet: any[] = [];
|
||||
@@ -178,7 +182,6 @@
|
||||
});
|
||||
return newRet;
|
||||
}
|
||||
|
||||
//重置
|
||||
function resetColumns() {
|
||||
state.checkList = [...state.defaultCheckList];
|
||||
@@ -193,7 +196,6 @@
|
||||
setColumns(newColumns);
|
||||
columnsList.value = newColumns;
|
||||
}
|
||||
|
||||
//全选
|
||||
function onCheckAll(e) {
|
||||
let checkList = table.getCacheColumns(true);
|
||||
@@ -205,33 +207,28 @@
|
||||
state.checkList = [];
|
||||
}
|
||||
}
|
||||
|
||||
//拖拽排序
|
||||
function draggableEnd() {
|
||||
const newColumns = toRaw(unref(columnsList));
|
||||
columnsList.value = newColumns;
|
||||
setColumns(newColumns);
|
||||
}
|
||||
|
||||
//勾选列
|
||||
function onSelection(e) {
|
||||
console.log('onSelection:' + JSON.stringify(e));
|
||||
let checkList = table.getCacheColumns();
|
||||
if (e) {
|
||||
if (checkList[0].type === undefined || checkList[0].type !== 'selection') {
|
||||
checkList.unshift({ type: 'selection', key: 'selection' });
|
||||
setColumns(checkList);
|
||||
}
|
||||
checkList.unshift({ type: 'selection', key: 'selection' });
|
||||
setColumns(checkList);
|
||||
} else {
|
||||
checkList.splice(0, 1);
|
||||
setColumns(checkList);
|
||||
}
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (e.draggedContext.element.draggable === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
//固定
|
||||
function fixedColumn(item, fixed) {
|
||||
if (!state.checkList.includes(item.key)) return;
|
||||
@@ -245,7 +242,6 @@
|
||||
columnsList.value[index].fixed = isFixed;
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
columnsList,
|
||||
@@ -268,65 +264,54 @@
|
||||
&-inner-popover-title {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
&-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar-inner {
|
||||
&-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
display: inline-flex;
|
||||
margin-right: 8px;
|
||||
cursor: move;
|
||||
|
||||
&-hidden {
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
flex: 1;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox-dark {
|
||||
&:hover {
|
||||
background: hsla(0, 0%, 100%, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-popover {
|
||||
.n-popover__content {
|
||||
padding: 0;
|
||||
|
||||
@@ -12,6 +12,14 @@ export const basicProps = {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showTopRight: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
openChecked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
|
||||
@@ -11,12 +11,24 @@
|
||||
>
|
||||
<div class="upload-card-item-info">
|
||||
<div class="img-box">
|
||||
<img :src="item" />
|
||||
<template v-if="fileType === 'image'">
|
||||
<img :src="item" @error="errorImg($event)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-avatar :style="fileAvatarCSS">{{ getFileExt(item) }}</n-avatar>
|
||||
</template>
|
||||
</div>
|
||||
<div class="img-box-actions">
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="preview(item)">
|
||||
<EyeOutlined />
|
||||
</n-icon>
|
||||
<template v-if="fileType === 'image'">
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="preview(item)">
|
||||
<EyeOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="download(item)">
|
||||
<CloudDownloadOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="remove(index)">
|
||||
<DeleteOutlined />
|
||||
</n-icon>
|
||||
@@ -40,7 +52,7 @@
|
||||
<n-icon size="18" class="m-auto">
|
||||
<PlusOutlined />
|
||||
</n-icon>
|
||||
<span class="upload-title">上传图片</span>
|
||||
<span class="upload-title">{{ uploadTitle }}</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
</div>
|
||||
@@ -68,21 +80,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
|
||||
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
|
||||
import { defineComponent, toRefs, reactive, computed, watch, onMounted, ref } from 'vue';
|
||||
import { EyeOutlined, DeleteOutlined, PlusOutlined, CloudDownloadOutlined } from '@vicons/antd';
|
||||
import { basicProps } from './props';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import componentSetting from '@/settings/componentSetting';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { isString } from '@/utils/is';
|
||||
|
||||
import { isJsonString, isNullOrUnDef } from '@/utils/is';
|
||||
import { getFileExt } from '@/utils/urlUtils';
|
||||
const globSetting = useGlobSetting();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
|
||||
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
|
||||
components: { EyeOutlined, DeleteOutlined, PlusOutlined, CloudDownloadOutlined },
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
@@ -97,6 +109,13 @@
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const uploadTitle = ref(props.fileType === 'image' ? '上传图片' : '上传附件');
|
||||
const fileAvatarCSS = computed(() => {
|
||||
return {
|
||||
'--n-merged-size': `var(--n-avatar-size-override, ${props.width * 0.8}px)`,
|
||||
'--n-font-size': `18px`,
|
||||
};
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
showModal: false,
|
||||
@@ -109,32 +128,55 @@
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
// console.log('props.value:' + props.value);
|
||||
// 单图模式
|
||||
if (typeof props.value === 'string') {
|
||||
let data: string[] = [];
|
||||
if (props.value !== '') {
|
||||
data.push(props.value);
|
||||
}
|
||||
|
||||
state.imgList = data.map((item) => {
|
||||
return getImgUrl(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 多图模式
|
||||
state.imgList = props.value.map((item) => {
|
||||
return getImgUrl(item);
|
||||
});
|
||||
loadValue(props.value);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.values,
|
||||
() => {
|
||||
loadValue(props.values);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
// 加载默认
|
||||
function loadValue(value: any) {
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: string[] = [];
|
||||
if (isJsonString(value)) {
|
||||
value = JSON.parse(value);
|
||||
}
|
||||
|
||||
// 单图模式
|
||||
if (typeof value === 'string') {
|
||||
if (value !== '') {
|
||||
data.push(value);
|
||||
}
|
||||
} else {
|
||||
// 多图模式
|
||||
data = value;
|
||||
}
|
||||
|
||||
state.imgList = data.map((item) => {
|
||||
return getImgUrl(item);
|
||||
});
|
||||
state.originalImgList = state.imgList;
|
||||
}
|
||||
|
||||
//预览
|
||||
function preview(url: string) {
|
||||
state.showModal = true;
|
||||
state.previewUrl = url;
|
||||
}
|
||||
//下载
|
||||
function download(url: string) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
//删除
|
||||
function remove(index: number) {
|
||||
@@ -146,7 +188,11 @@
|
||||
onPositiveClick: () => {
|
||||
state.imgList.splice(index, 1);
|
||||
state.originalImgList.splice(index, 1);
|
||||
emit('uploadChange', state.originalImgList);
|
||||
if (props.maxNumber === 1) {
|
||||
emit('uploadChange', '');
|
||||
} else {
|
||||
emit('uploadChange', state.originalImgList);
|
||||
}
|
||||
emit('delete', state.originalImgList);
|
||||
},
|
||||
onNegativeClick: () => {},
|
||||
@@ -159,25 +205,29 @@
|
||||
return /(^http|https:\/\/)/g.test(url) ? url : `${imgUrl}${url}`;
|
||||
}
|
||||
|
||||
function checkFileType(fileType: string) {
|
||||
return componentSetting.upload.fileType.includes(fileType);
|
||||
function checkFileType(map: string[], fileType: string) {
|
||||
if (isNullOrUnDef(map)) {
|
||||
return true;
|
||||
}
|
||||
return map.includes(fileType);
|
||||
}
|
||||
|
||||
//上传之前
|
||||
function beforeUpload({ file }) {
|
||||
const fileInfo = file.file;
|
||||
const { maxSize, accept } = props;
|
||||
const acceptRef = (isString(accept) && accept.split(',')) || [];
|
||||
|
||||
// 设置最大值,则判断
|
||||
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
|
||||
message.error(`上传文件最大值不能超过${maxSize}M`);
|
||||
if (props.maxSize && fileInfo.size / 1024 / 1024 >= props.maxSize) {
|
||||
message.error(`上传文件最大值不能超过${props.maxSize}M`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置类型,则判断
|
||||
const fileType = componentSetting.upload.fileType;
|
||||
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
|
||||
const fileType =
|
||||
props.fileType === 'image'
|
||||
? componentSetting.upload.imageType
|
||||
: componentSetting.upload.fileType;
|
||||
if (!checkFileType(fileType, fileInfo.type)) {
|
||||
console.log('checkFileType fileInfo.type:' + fileInfo.type);
|
||||
message.error(`只能上传文件类型为${fileType.join(',')}`);
|
||||
return false;
|
||||
}
|
||||
@@ -197,20 +247,45 @@
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
let imgUrl: string = getImgUrl(result[imgField]);
|
||||
state.imgList.push(imgUrl);
|
||||
state.originalImgList.push(result[imgField]);
|
||||
emit('uploadChange', state.originalImgList);
|
||||
state.originalImgList = state.imgList;
|
||||
if (props.maxNumber === 1) {
|
||||
emit('uploadChange', imgUrl);
|
||||
} else {
|
||||
emit('uploadChange', state.originalImgList);
|
||||
}
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**图片加载失败显示自定义默认图片(缺省图)*/
|
||||
function errorImg(e) {
|
||||
e.srcElement.src = '/onerror.png';
|
||||
//这一句没用,如果默认图片的路径错了还是会一直闪屏,在方法的前面加个.once只让它执行一次也没用
|
||||
e.srcElement.onerror = null; //防止闪图
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setTimeout(function () {
|
||||
if (props.maxNumber === 1) {
|
||||
loadValue(props.value);
|
||||
} else {
|
||||
loadValue(props.values);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
return {
|
||||
errorImg,
|
||||
...toRefs(state),
|
||||
finish,
|
||||
preview,
|
||||
download,
|
||||
remove,
|
||||
beforeUpload,
|
||||
getCSSProperties,
|
||||
uploadTitle,
|
||||
fileAvatarCSS,
|
||||
getFileExt,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,10 @@ import { NUpload } from 'naive-ui';
|
||||
|
||||
export const basicProps = {
|
||||
...NUpload.props,
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '.jpg,.png,.jpeg,.svg,.gif',
|
||||
@@ -24,7 +28,7 @@ export const basicProps = {
|
||||
default: () => '',
|
||||
},
|
||||
values: {
|
||||
type: (Array as PropType<string[]>) || (String as PropType<string>),
|
||||
type: (Array as PropType<string[]>) || (Object as PropType<object>),
|
||||
default: () => [],
|
||||
},
|
||||
width: {
|
||||
|
||||
59
web/src/components/Upload/uploadFile.vue
Normal file
59
web/src/components/Upload/uploadFile.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<BasicUpload
|
||||
:action="`${uploadUrl}${urlPrefix}/upload/file`"
|
||||
:headers="uploadHeaders"
|
||||
:data="{ type: 0 }"
|
||||
name="file"
|
||||
:width="100"
|
||||
:height="100"
|
||||
fileType="file"
|
||||
:maxNumber="maxNumber"
|
||||
@uploadChange="uploadChange"
|
||||
v-model:value="image"
|
||||
v-model:values="images"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, unref, reactive } from 'vue';
|
||||
import { BasicUpload } from '@/components/Upload';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
|
||||
export interface Props {
|
||||
value: string | string[] | null;
|
||||
maxNumber: number;
|
||||
}
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix || '';
|
||||
const { uploadUrl } = globSetting;
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
const uploadHeaders = reactive({
|
||||
Authorization: useUserStore.token,
|
||||
});
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = withDefaults(defineProps<Props>(), { value: '', maxNumber: 1 });
|
||||
const image = ref<string>('');
|
||||
const images = ref<string[] | object>([]);
|
||||
|
||||
function uploadChange(list: string | string[]) {
|
||||
if (props.maxNumber === 1) {
|
||||
image.value = unref(list as string);
|
||||
emit('update:value', image.value);
|
||||
} else {
|
||||
images.value = unref(list as string[]);
|
||||
emit('update:value', images.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.maxNumber === 1) {
|
||||
image.value = props.value as string;
|
||||
} else {
|
||||
images.value = props.value as string[];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
58
web/src/components/Upload/uploadImage.vue
Normal file
58
web/src/components/Upload/uploadImage.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<BasicUpload
|
||||
:action="`${uploadUrl}${urlPrefix}/upload/image`"
|
||||
:headers="uploadHeaders"
|
||||
:data="{ type: 0 }"
|
||||
name="file"
|
||||
:width="100"
|
||||
:height="100"
|
||||
:maxNumber="maxNumber"
|
||||
@uploadChange="uploadChange"
|
||||
v-model:value="image"
|
||||
v-model:values="images"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, unref, reactive } from 'vue';
|
||||
import { BasicUpload } from '@/components/Upload';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
|
||||
export interface Props {
|
||||
value: string | string[] | null;
|
||||
maxNumber: number;
|
||||
}
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix || '';
|
||||
const { uploadUrl } = globSetting;
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
const uploadHeaders = reactive({
|
||||
Authorization: useUserStore.token,
|
||||
});
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = withDefaults(defineProps<Props>(), { value: '', maxNumber: 1 });
|
||||
const image = ref<string>('');
|
||||
const images = ref<string[]>([]);
|
||||
|
||||
function uploadChange(list: string | string[]) {
|
||||
if (props.maxNumber === 1) {
|
||||
image.value = unref(list as string);
|
||||
emit('update:value', image.value);
|
||||
} else {
|
||||
images.value = unref(list as string[]);
|
||||
emit('update:value', images.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.maxNumber === 1) {
|
||||
image.value = props.value as string;
|
||||
} else {
|
||||
images.value = props.value as string[];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
Reference in New Issue
Block a user