转为使用element-plus
This commit is contained in:
@@ -1,320 +0,0 @@
|
||||
<template>
|
||||
<a-form :model="formData" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol" :layout="layout"
|
||||
@finish="handleFinish" @finish-failed="handleFinishFailed">
|
||||
<a-form-item v-for="item in formItems" :key="item.field" :label="item.label" :name="item.field"
|
||||
:required="item.required" :colon="item.colon">
|
||||
<!-- 输入框 -->
|
||||
<template v-if="item.type === 'input'">
|
||||
<a-input v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
|
||||
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :max-length="item.maxLength"
|
||||
:type="item.inputType || 'text'" :prefix="item.prefix" :suffix="item.suffix"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 文本域 -->
|
||||
<template v-else-if="item.type === 'textarea'">
|
||||
<a-textarea v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
|
||||
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :rows="item.rows || 4"
|
||||
:max-length="item.maxLength" :show-count="item.showCount"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<template v-else-if="item.type === 'password'">
|
||||
<a-input-password v-model:value="formData[item.field]"
|
||||
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled"
|
||||
:max-length="item.maxLength" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 数字输入框 -->
|
||||
<template v-else-if="item.type === 'number'">
|
||||
<a-input-number v-model:value="formData[item.field]"
|
||||
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled" :min="item.min"
|
||||
:max="item.max" :step="item.step || 1" :precision="item.precision"
|
||||
:controls="item.controls !== false" style="width: 100%"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 下拉选择 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<a-select v-model:value="formData[item.field]" :placeholder="item.placeholder || `请选择${item.label}`"
|
||||
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :mode="item.mode"
|
||||
:options="item.options" :field-names="item.fieldNames" style="width: 100%"
|
||||
@change="item.onChange && item.onChange(formData[item.field])">
|
||||
<template v-if="!item.options" #notFoundContent>
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="暂无数据" />
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<!-- 单选框 -->
|
||||
<template v-else-if="item.type === 'radio'">
|
||||
<a-radio-group v-model:value="formData[item.field]" :disabled="item.disabled"
|
||||
:button-style="item.buttonStyle" @change="item.onChange && item.onChange(formData[item.field])">
|
||||
<template v-if="item.options">
|
||||
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||
:disabled="opt.disabled">
|
||||
{{ opt.label }}
|
||||
</a-radio>
|
||||
</template>
|
||||
<template v-else-if="item.buttonStyle === 'solid'">
|
||||
<a-radio-button v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||
:disabled="opt.disabled">
|
||||
{{ opt.label }}
|
||||
</a-radio-button>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<!-- 多选框 -->
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<a-checkbox-group v-model:value="formData[item.field]" :disabled="item.disabled"
|
||||
@change="item.onChange && item.onChange(formData[item.field])">
|
||||
<template v-if="item.options">
|
||||
<a-checkbox v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||
:disabled="opt.disabled">
|
||||
{{ opt.label }}
|
||||
</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</template>
|
||||
|
||||
<!-- 开关 -->
|
||||
<template v-else-if="item.type === 'switch'">
|
||||
<a-switch v-model:checked="formData[item.field]" :disabled="item.disabled"
|
||||
:checked-children="item.checkedChildren || '开'" :un-checked-children="item.unCheckedChildren || '关'"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<template v-else-if="item.type === 'date'">
|
||||
<a-date-picker v-model:value="formData[item.field]"
|
||||
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||
:format="item.format || 'YYYY-MM-DD'" :value-format="item.valueFormat || 'YYYY-MM-DD'"
|
||||
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 日期范围选择 -->
|
||||
<template v-else-if="item.type === 'dateRange'">
|
||||
<a-range-picker v-model:value="formData[item.field]" :placeholder="item.placeholder || ['开始日期', '结束日期']"
|
||||
:disabled="item.disabled" :format="item.format || 'YYYY-MM-DD'"
|
||||
:value-format="item.valueFormat || 'YYYY-MM-DD'" style="width: 100%"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 时间选择 -->
|
||||
<template v-else-if="item.type === 'time'">
|
||||
<a-time-picker v-model:value="formData[item.field]"
|
||||
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||
:format="item.format || 'HH:mm:ss'" :value-format="item.valueFormat || 'HH:mm:ss'"
|
||||
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 上传 -->
|
||||
<template v-else-if="item.type === 'upload'">
|
||||
<a-upload v-model:file-list="formData[item.field]" :list-type="item.listType || 'text'"
|
||||
:action="item.action" :max-count="item.maxCount" :before-upload="item.beforeUpload"
|
||||
:custom-request="item.customRequest" :accept="item.accept" :disabled="item.disabled"
|
||||
@change="(info) => item.onChange && item.onChange(info)">
|
||||
<a-button v-if="item.listType !== 'picture-card'" type="primary">
|
||||
<UploadOutlined />
|
||||
点击上传
|
||||
</a-button>
|
||||
<div v-else>
|
||||
<PlusOutlined />
|
||||
<div class="ant-upload-text">上传</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</template>
|
||||
|
||||
<!-- 评分 -->
|
||||
<template v-else-if="item.type === 'rate'">
|
||||
<a-rate v-model:value="formData[item.field]" :disabled="item.disabled" :count="item.count || 5"
|
||||
:allow-half="item.allowHalf" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 滑块 -->
|
||||
<template v-else-if="item.type === 'slider'">
|
||||
<a-slider v-model:value="formData[item.field]" :disabled="item.disabled" :min="item.min || 0"
|
||||
:max="item.max || 100" :step="item.step || 1" :marks="item.marks" :range="item.range"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 级联选择 -->
|
||||
<template v-else-if="item.type === 'cascader'">
|
||||
<a-cascader v-model:value="formData[item.field]" :options="item.options"
|
||||
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||
:change-on-select="item.changeOnSelect" :field-names="item.fieldNames" style="width: 100%"
|
||||
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||
</template>
|
||||
|
||||
<!-- 自定义插槽 -->
|
||||
<template v-else-if="item.type === 'slot'">
|
||||
<slot :name="item.slotName || item.field" :field="item.field" :value="formData[item.field]"></slot>
|
||||
</template>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<template v-if="item.tip">
|
||||
<div class="form-item-tip">{{ item.tip }}</div>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 表单操作按钮 -->
|
||||
<a-form-item v-if="showActions" :wrapper-col="actionWrapperCol">
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading" :size="buttonSize">
|
||||
{{ submitText || '提交' }}
|
||||
</a-button>
|
||||
<a-button v-if="showReset" @click="handleReset" :size="buttonSize">
|
||||
{{ resetText || '重置' }}
|
||||
</a-button>
|
||||
<a-button v-if="showCancel" @click="handleCancel" :size="buttonSize">
|
||||
{{ cancelText || '取消' }}
|
||||
</a-button>
|
||||
<slot name="actions"></slot>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 自定义插槽 -->
|
||||
<slot></slot>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Empty } from 'ant-design-vue'
|
||||
import { UploadOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
// 表单项配置
|
||||
formItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
// 表单初始值
|
||||
initialValues: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 表单布局
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'horizontal', // horizontal, vertical, inline
|
||||
},
|
||||
// 标签宽度
|
||||
labelCol: {
|
||||
type: Object,
|
||||
default: () => ({ span: 6 }),
|
||||
},
|
||||
// 内容宽度
|
||||
wrapperCol: {
|
||||
type: Object,
|
||||
default: () => ({ span: 16 }),
|
||||
},
|
||||
// 是否显示操作按钮
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 操作按钮布局
|
||||
actionWrapperCol: {
|
||||
type: Object,
|
||||
default: () => ({ offset: 6, span: 16 }),
|
||||
},
|
||||
// 是否显示重置按钮
|
||||
showReset: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示取消按钮
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 按钮文字
|
||||
submitText: String,
|
||||
resetText: String,
|
||||
cancelText: String,
|
||||
// 按钮大小
|
||||
buttonSize: {
|
||||
type: String,
|
||||
default: 'middle',
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['finish', 'finish-failed', 'reset', 'cancel'])
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({ ...props.initialValues })
|
||||
|
||||
// 表单验证规则
|
||||
const rules = computed(() => {
|
||||
const result = {}
|
||||
props.formItems.forEach((item) => {
|
||||
if (item.rules && item.rules.length > 0) {
|
||||
result[item.field] = item.rules
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
// 监听初始值变化
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newVal) => {
|
||||
Object.assign(formData, newVal)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 表单提交
|
||||
const handleFinish = (values) => {
|
||||
emit('finish', values)
|
||||
}
|
||||
|
||||
// 表单验证失败
|
||||
const handleFinishFailed = (errorInfo) => {
|
||||
emit('finish-failed', errorInfo)
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formData, props.initialValues)
|
||||
emit('reset', formData)
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
formData,
|
||||
resetForm: handleReset,
|
||||
setFieldValue: (field, value) => {
|
||||
formData[field] = value
|
||||
},
|
||||
getFieldValue: (field) => {
|
||||
return formData[field]
|
||||
},
|
||||
setFieldsValue: (values) => {
|
||||
Object.assign(formData, values)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-item-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,508 +0,0 @@
|
||||
<template>
|
||||
<div class="sc-icon-picker">
|
||||
<a-input :value="selectedIcon ? '' : ''" :placeholder="placeholder" readonly @click="handleOpenPicker">
|
||||
<template #prefix v-if="selectedIcon">
|
||||
<component :is="selectedIcon" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<SearchOutlined v-if="!selectedIcon" />
|
||||
<CloseCircleFilled v-else @click.stop="handleClear" />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-modal v-model:open="visible" title="选择图标" :width="800" :footer="null" @cancel="handleCancel">
|
||||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||
<a-tab-pane key="antd" tab="Ant Design">
|
||||
<div class="icon-search">
|
||||
<a-input v-model:value="searchAntdValue" placeholder="搜索图标..." allow-clear>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="icon-list">
|
||||
<div
|
||||
v-for="icon in filteredAntdIcons"
|
||||
:key="icon"
|
||||
:class="['icon-item', { active: tempIcon === icon }]"
|
||||
@click="handleSelectIcon(icon)"
|
||||
>
|
||||
<component :is="icon" />
|
||||
<div class="icon-name">{{ icon }}</div>
|
||||
</div>
|
||||
<a-empty v-if="filteredAntdIcons.length === 0" description="暂无图标" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="element" tab="Element Plus">
|
||||
<div class="icon-search">
|
||||
<a-input v-model:value="searchElementValue" placeholder="搜索图标..." allow-clear>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="icon-list">
|
||||
<div
|
||||
v-for="icon in filteredElementIcons"
|
||||
:key="icon"
|
||||
:class="['icon-item', { active: tempIcon === icon }]"
|
||||
@click="handleSelectIcon(icon)"
|
||||
>
|
||||
<component :is="icon" />
|
||||
<div class="icon-name">{{ icon.replace('El', '') }}</div>
|
||||
</div>
|
||||
<a-empty v-if="filteredElementIcons.length === 0" description="暂无图标" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* @component scIconPicker
|
||||
*/
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Empty } from 'ant-design-vue'
|
||||
import { SearchOutlined, CloseCircleFilled } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择图标',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('antd')
|
||||
const searchAntdValue = ref('')
|
||||
const searchElementValue = ref('')
|
||||
const tempIcon = ref('')
|
||||
|
||||
// Ant Design 图标列表(常用图标)
|
||||
const antdIcons = [
|
||||
'HomeOutlined',
|
||||
'UserOutlined',
|
||||
'SettingOutlined',
|
||||
'EditOutlined',
|
||||
'DeleteOutlined',
|
||||
'PlusOutlined',
|
||||
'MinusOutlined',
|
||||
'CheckOutlined',
|
||||
'CloseOutlined',
|
||||
'SearchOutlined',
|
||||
'FilterOutlined',
|
||||
'ReloadOutlined',
|
||||
'DownloadOutlined',
|
||||
'UploadOutlined',
|
||||
'FileTextOutlined',
|
||||
'FolderOutlined',
|
||||
'PictureOutlined',
|
||||
'VideoCameraOutlined',
|
||||
'AudioOutlined',
|
||||
'FileOutlined',
|
||||
'CalendarOutlined',
|
||||
'ClockCircleOutlined',
|
||||
'HeartOutlined',
|
||||
'StarOutlined',
|
||||
'ThumbUpOutlined',
|
||||
'MessageOutlined',
|
||||
'PhoneOutlined',
|
||||
'MailOutlined',
|
||||
'EnvironmentOutlined',
|
||||
'GlobalOutlined',
|
||||
'LinkOutlined',
|
||||
'LockOutlined',
|
||||
'UnlockOutlined',
|
||||
'EyeOutlined',
|
||||
'EyeInvisibleOutlined',
|
||||
'ArrowLeftOutlined',
|
||||
'ArrowRightOutlined',
|
||||
'ArrowUpOutlined',
|
||||
'ArrowDownOutlined',
|
||||
'CaretLeftOutlined',
|
||||
'CaretRightOutlined',
|
||||
'CaretUpOutlined',
|
||||
'CaretDownOutlined',
|
||||
'LeftOutlined',
|
||||
'RightOutlined',
|
||||
'UpOutlined',
|
||||
'DownOutlined',
|
||||
'MenuFoldOutlined',
|
||||
'MenuUnfoldOutlined',
|
||||
'BarsOutlined',
|
||||
'MoreOutlined',
|
||||
'EllipsisOutlined',
|
||||
'DashboardOutlined',
|
||||
'AppstoreOutlined',
|
||||
'LaptopOutlined',
|
||||
'DesktopOutlined',
|
||||
'TabletOutlined',
|
||||
'MobileOutlined',
|
||||
'WifiOutlined',
|
||||
'BluetoothOutlined',
|
||||
'ThunderboltOutlined',
|
||||
'BulbOutlined',
|
||||
'SoundOutlined',
|
||||
'NotificationOutlined',
|
||||
'BellOutlined',
|
||||
'AlertOutlined',
|
||||
'WarningOutlined',
|
||||
'InfoCircleOutlined',
|
||||
'QuestionCircleOutlined',
|
||||
'CheckCircleOutlined',
|
||||
'CloseCircleOutlined',
|
||||
'StopOutlined',
|
||||
'ExclamationCircleOutlined',
|
||||
'SafetyOutlined',
|
||||
'ShieldCheckOutlined',
|
||||
'SecurityScanOutlined',
|
||||
'KeyOutlined',
|
||||
'IdcardOutlined',
|
||||
'ProfileOutlined',
|
||||
'SolutionOutlined',
|
||||
'ContactsOutlined',
|
||||
'TeamOutlined',
|
||||
'UsergroupAddOutlined',
|
||||
'UsergroupDeleteOutlined',
|
||||
'CrownOutlined',
|
||||
'GoldOutlined',
|
||||
'MoneyCollectOutlined',
|
||||
'BankOutlined',
|
||||
'PayCircleOutlined',
|
||||
'CreditCardOutlined',
|
||||
'WalletOutlined',
|
||||
'ShoppingCartOutlined',
|
||||
'ShoppingOutlined',
|
||||
'GiftOutlined',
|
||||
'HddOutlined',
|
||||
'DatabaseOutlined',
|
||||
'CloudOutlined',
|
||||
'CloudUploadOutlined',
|
||||
'CloudDownloadOutlined',
|
||||
'ServerOutlined',
|
||||
'AuditOutlined',
|
||||
'NodeIndexOutlined',
|
||||
'ReconciliationOutlined',
|
||||
'PartitionOutlined',
|
||||
'AccountBookOutlined',
|
||||
'ProjectOutlined',
|
||||
'ControlOutlined',
|
||||
'MonitorOutlined',
|
||||
'TagsOutlined',
|
||||
'TagOutlined',
|
||||
'BookOutlined',
|
||||
'ReadOutlined',
|
||||
'ExperimentOutlined',
|
||||
'FireOutlined',
|
||||
'RocketOutlined',
|
||||
'TrophyOutlined',
|
||||
'MedalOutlined',
|
||||
'DiamondOutlined',
|
||||
'ThunderboltTwoTone',
|
||||
]
|
||||
|
||||
// Element Plus 图标列表(常用图标)
|
||||
const elementIcons = [
|
||||
'ElIconEdit',
|
||||
'ElIconDelete',
|
||||
'ElIconSearch',
|
||||
'ElIconClose',
|
||||
'ElIconCheck',
|
||||
'ElIconPlus',
|
||||
'ElIconMinus',
|
||||
'ElIconUpload',
|
||||
'ElIconDownload',
|
||||
'ElIconSetting',
|
||||
'ElIconRefresh',
|
||||
'ElIconRefreshLeft',
|
||||
'ElIconRefreshRight',
|
||||
'ElIconMenu',
|
||||
'ElIconMore',
|
||||
'ElIconMoreFilled',
|
||||
'ElIconStar',
|
||||
'ElIconStarFilled',
|
||||
'ElIconSunny',
|
||||
'ElIconMoon',
|
||||
'ElIconBell',
|
||||
'ElIconBellFilled',
|
||||
'ElIconMessage',
|
||||
'ElIconMessageFilled',
|
||||
'ElIconChatDotRound',
|
||||
'ElIconChatLineSquare',
|
||||
'ElIconChatDotSquare',
|
||||
'ElIconPhone',
|
||||
'ElIconPhoneFilled',
|
||||
'ElIconLocation',
|
||||
'ElIconLocationFilled',
|
||||
'ElIconLocationInformation',
|
||||
'ElIconView',
|
||||
'ElIconHide',
|
||||
'ElIconLock',
|
||||
'ElIconUnlock',
|
||||
'ElIconKey',
|
||||
'ElIconTickets',
|
||||
'ElIconDocument',
|
||||
'ElIconDocumentAdd',
|
||||
'ElIconDocumentDelete',
|
||||
'ElIconDocumentCopy',
|
||||
'ElIconDocumentChecked',
|
||||
'ElIconDocumentRemove',
|
||||
'ElIconFolder',
|
||||
'ElIconFolderOpened',
|
||||
'ElIconFolderAdd',
|
||||
'ElIconFolderDelete',
|
||||
'ElIconFolderChecked',
|
||||
'ElIconFiles',
|
||||
'ElIconPicture',
|
||||
'ElIconPictureRounded',
|
||||
'ElIconPictureFilled',
|
||||
'ElIconVideoCamera',
|
||||
'ElIconVideoCameraFilled',
|
||||
'ElIconMicrophone',
|
||||
'ElIconMicrophoneFilled',
|
||||
'ElIconHeadset',
|
||||
'ElIconHeadsetFilled',
|
||||
'ElIconMuteNotification',
|
||||
'ElIconNotification',
|
||||
'ElIconWarning',
|
||||
'ElIconWarningFilled',
|
||||
'ElIconInfoFilled',
|
||||
'ElIconSuccessFilled',
|
||||
'ElIconCircleCheck',
|
||||
'ElIconCircleCheckFilled',
|
||||
'ElIconCircleClose',
|
||||
'ElIconCircleCloseFilled',
|
||||
'ElIconCirclePlus',
|
||||
'ElIconCirclePlusFilled',
|
||||
'ElIconCircleMinus',
|
||||
'ElIconCircleMinusFilled',
|
||||
'ElIconAim',
|
||||
'ElIconPosition',
|
||||
'ElIconCompass',
|
||||
'ElIconMapLocation',
|
||||
'ElIconPromotion',
|
||||
'ElIconDownload',
|
||||
'ElIconUploadFilled',
|
||||
'ElIconShare',
|
||||
'ElIconConnection',
|
||||
'ElIconLink',
|
||||
'ElIconUnlink',
|
||||
'ElIconOperation',
|
||||
'ElIconDataAnalysis',
|
||||
'ElIconDataLine',
|
||||
'ElIconDataBoard',
|
||||
'ElIconHistogram',
|
||||
'ElIconTrendCharts',
|
||||
'ElIconPieChart',
|
||||
'ElIconOdometer',
|
||||
'ElIconMonitor',
|
||||
'ElIconTimer',
|
||||
'ElIconClock',
|
||||
'ElIconAlarmClock',
|
||||
'ElIconCalendar',
|
||||
'ElIconDate',
|
||||
'ElIconSwitch',
|
||||
'ElIconSwitchButton',
|
||||
'ElIconTools',
|
||||
'ElIconScrewdriver',
|
||||
'ElIconHammer',
|
||||
'ElIconBrush',
|
||||
'ElIconEditPen',
|
||||
'ElIconBriefcase',
|
||||
'ElIconWallet',
|
||||
'ElIconGoods',
|
||||
'ElIconShoppingCart',
|
||||
'ElIconShoppingCartFull',
|
||||
'ElIconShoppingBag',
|
||||
'ElIconPresent',
|
||||
'ElIconSoldOut',
|
||||
'ElIconSell',
|
||||
'ElIconDiscount',
|
||||
'ElIconTicket',
|
||||
'ElIconCoin',
|
||||
'ElIconMoney',
|
||||
'ElIconWalletFilled',
|
||||
'ElIconCreditCard',
|
||||
'ElIconUser',
|
||||
'ElIconUserFilled',
|
||||
'ElIconAvatar',
|
||||
'ElIconSuitcase',
|
||||
'ElIconGrid',
|
||||
'ElIconMenuFilled',
|
||||
'ElIconHomeFilled',
|
||||
'ElIconHouse',
|
||||
'ElIconOfficeBuilding',
|
||||
'ElIconSchool',
|
||||
'ElIconReading',
|
||||
'ElIconReadingLamp',
|
||||
'ElIconNotebook',
|
||||
'ElIconNotebookFilled',
|
||||
'ElIconFinished',
|
||||
'ElIconCollection',
|
||||
'ElIconCollectionTag',
|
||||
'ElIconFiles',
|
||||
'ElIconPostcard',
|
||||
'ElIconMemo',
|
||||
'ElIconStamp',
|
||||
'ElIconPriceTag',
|
||||
'ElIconMedal',
|
||||
'ElIconTrophy',
|
||||
'ElIconTrophyBase',
|
||||
'ElIconFirstAidKit',
|
||||
'ElIconToiletPaper',
|
||||
'ElIconAim',
|
||||
'ElIconSFlag',
|
||||
'ElIconSOpportunity',
|
||||
'ElIconMagicStick',
|
||||
'ElIconHelp',
|
||||
'ElIconQuestionFilled',
|
||||
'ElIconWarning',
|
||||
'ElIconWarningFilled',
|
||||
]
|
||||
|
||||
// 当前选中的图标
|
||||
const selectedIcon = ref(props.modelValue)
|
||||
|
||||
// 过滤后的 Ant Design 图标
|
||||
const filteredAntdIcons = computed(() => {
|
||||
if (!searchAntdValue.value) {
|
||||
return antdIcons
|
||||
}
|
||||
return antdIcons.filter((icon) =>
|
||||
icon.toLowerCase().includes(searchAntdValue.value.toLowerCase()),
|
||||
)
|
||||
})
|
||||
|
||||
// 过滤后的 Element 图标
|
||||
const filteredElementIcons = computed(() => {
|
||||
if (!searchElementValue.value) {
|
||||
return elementIcons
|
||||
}
|
||||
return elementIcons.filter((icon) =>
|
||||
icon.toLowerCase().includes(searchElementValue.value.toLowerCase()),
|
||||
)
|
||||
})
|
||||
|
||||
// 打开选择器
|
||||
const handleOpenPicker = () => {
|
||||
tempIcon.value = props.modelValue
|
||||
// 根据当前图标设置默认标签页
|
||||
if (props.modelValue) {
|
||||
activeTab.value = props.modelValue.startsWith('El') ? 'element' : 'antd'
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 清除选择
|
||||
const handleClear = () => {
|
||||
emit('update:modelValue', '')
|
||||
emit('change', '')
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const handleTabChange = (key) => {
|
||||
activeTab.value = key
|
||||
}
|
||||
|
||||
// 选择图标(直接确认并关闭)
|
||||
const handleSelectIcon = (icon) => {
|
||||
emit('update:modelValue', icon)
|
||||
emit('change', icon)
|
||||
selectedIcon.value = icon
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 取消选择
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 监听props变化,更新本地状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
selectedIcon.value = newVal
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sc-icon-picker {
|
||||
:deep(.ant-input) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,546 +0,0 @@
|
||||
<template>
|
||||
<div class="sc-table" ref="tableWrapper">
|
||||
<!-- 表格内容 -->
|
||||
<div class="sc-table-content" ref="tableContent">
|
||||
<a-table :columns="tableColumns" :data-source="dataSource" :loading="loading" :pagination="false"
|
||||
:row-key="rowKey" :row-selection="rowSelection" :scroll="scroll" :bordered="tableSettings.bordered"
|
||||
:size="tableSettings.size" :show-header="showHeader" :locale="locale" @change="handleTableChange"
|
||||
@resizeColumn="handleResizeColumn">
|
||||
<!-- 自定义单元格内容 -->
|
||||
<template #bodyCell="{ text, record, index, column }">
|
||||
<!-- 序号列 -->
|
||||
<template v-if="column.dataIndex === '_index'">
|
||||
{{ getTableIndex(index) }}
|
||||
</template>
|
||||
<!-- 自定义插槽 -->
|
||||
<template v-else-if="column.slot">
|
||||
<slot :name="column.slot || column.dataIndex" :text="text" :record="record" :index="index"
|
||||
:column="column"></slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #emptyText>
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="emptyText" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div v-if="showToolbar" class="sc-table-tool">
|
||||
<div class="tool-left">
|
||||
<a-pagination v-bind="pagination" @change="handlePaginationChange"
|
||||
@showSizeChange="handlePaginationChange" />
|
||||
</div>
|
||||
<div class="tool-right">
|
||||
<!-- 右侧工具栏插槽 -->
|
||||
<slot name="toolRight"></slot>
|
||||
<!-- 刷新按钮 -->
|
||||
<a-tooltip v-if="showRefresh" title="刷新">
|
||||
<a-button shape="circle" :loading="loading" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 表格设置按钮 -->
|
||||
<a-tooltip v-if="showColumnSetting" title="表格设置">
|
||||
<a-popover v-model:open="tableSettingVisible" placement="topRight" trigger="click" :width="240">
|
||||
<template #content>
|
||||
<div class="table-setting">
|
||||
<div class="table-setting-header">
|
||||
<span>表格设置</span>
|
||||
</div>
|
||||
<div class="table-setting-body">
|
||||
<!-- 边框设置 -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">显示边框</span>
|
||||
<a-switch v-model:checked="tableSettings.bordered" size="small" />
|
||||
</div>
|
||||
<!-- 表格大小 -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">表格大小</span>
|
||||
<a-radio-group v-model:value="tableSettings.size" size="small"
|
||||
button-style="solid">
|
||||
<a-radio-button value="small">小</a-radio-button>
|
||||
<a-radio-button value="middle">中</a-radio-button>
|
||||
<a-radio-button value="large">大</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button shape="circle">
|
||||
<template #icon>
|
||||
<TableOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 列设置按钮 -->
|
||||
<a-tooltip v-if="showColumnSetting" title="列设置">
|
||||
<a-popover v-model:open="columnSettingVisible" placement="topRight" trigger="click">
|
||||
<template #content>
|
||||
<div class="column-setting">
|
||||
<div class="column-setting-header">
|
||||
<span>显示与排序</span>
|
||||
</div>
|
||||
<div class="column-setting-list">
|
||||
<div v-for="(colKey, index) in sortedColumns" :key="colKey"
|
||||
class="column-setting-item" :class="{ dragging: draggingIndex === index }"
|
||||
draggable="true" @dragstart="handleDragStart(index, $event)"
|
||||
@dragover="handleDragOver(index, $event)" @dragend="handleDragEnd"
|
||||
@drop="handleDrop(index)">
|
||||
<HolderOutlined class="drag-handle" />
|
||||
<a-checkbox :checked="visibleColumns.includes(colKey)"
|
||||
@change="(e) => toggleColumn(colKey, e.target.checked)">
|
||||
{{ getColumnTitle(colKey) }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button shape="circle">
|
||||
<template #icon>
|
||||
<HolderOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, reactive, useTemplateRef, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Empty } from 'ant-design-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'scTable',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
// 数据源
|
||||
dataSource: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 列配置
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
// 行的唯一标识
|
||||
rowKey: {
|
||||
type: [String, Function],
|
||||
default: 'id',
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 分页配置
|
||||
pagination: {
|
||||
type: [Object, Boolean],
|
||||
default: () => ({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
pageSizeOptions: ['20', '50', '100', '200'],
|
||||
}),
|
||||
},
|
||||
// 行选择配置
|
||||
rowSelection: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
// 表格大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'middle', // large, middle, small
|
||||
},
|
||||
// 是否显示边框
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否显示表头
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 本地化配置
|
||||
locale: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 是否显示序号列
|
||||
showIndex: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 序号列宽度
|
||||
indexColumnWidth: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
// 序号列标题
|
||||
indexTitle: {
|
||||
type: String,
|
||||
default: '序号',
|
||||
},
|
||||
// 是否显示工具栏
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示刷新按钮
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示列设置
|
||||
showColumnSetting: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 空状态文字
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
})
|
||||
|
||||
const tableContent = useTemplateRef('tableContent')
|
||||
const tableWrapper = useTemplateRef('tableWrapper')
|
||||
let scroll = ref({
|
||||
scrollToFirstRowOnChange: true,
|
||||
x: 'max-content',
|
||||
y: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateTableHeight()
|
||||
})
|
||||
|
||||
const updateTableHeight = () => {
|
||||
if (tableContent.value) {
|
||||
const tableHeight = tableContent.value.clientHeight - 56
|
||||
scroll.value.y = tableHeight > 0 ? tableHeight : 400
|
||||
}
|
||||
}
|
||||
|
||||
// 根据表格宽度优化横向滚动配置
|
||||
watch(
|
||||
[() => props.columns, () => props.showIndex, tableContent],
|
||||
() => {
|
||||
// 如果列有固定宽度且总宽度较大,使用max-content
|
||||
// 否则使用true让表格自适应
|
||||
const hasFixedColumns = props.columns.some((col) => col.width)
|
||||
if (hasFixedColumns || props.showIndex) {
|
||||
scroll.value.x = 'max-content'
|
||||
} else {
|
||||
scroll.value.x = true
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// 表格设置状态
|
||||
const tableSettings = reactive({
|
||||
bordered: props.bordered,
|
||||
size: props.size,
|
||||
})
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.bordered,
|
||||
(val) => {
|
||||
tableSettings.bordered = val
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
(val) => {
|
||||
tableSettings.size = val
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits(['refresh', 'change', 'resizeColumn', 'select', 'selectAll', 'selectNone', 'paginationChange'])
|
||||
|
||||
// 列设置相关
|
||||
const columnSettingVisible = ref(false)
|
||||
const tableSettingVisible = ref(false)
|
||||
const visibleColumns = ref([])
|
||||
const sortedColumns = ref([]) // 排序后的列key数组
|
||||
const draggingIndex = ref(-1) // 当前拖拽的索引
|
||||
|
||||
// 所有列
|
||||
const allColumns = computed(() => {
|
||||
return props.columns.filter((col) => col.dataIndex && col.dataIndex !== '_index')
|
||||
})
|
||||
|
||||
// 获取列标题
|
||||
const getColumnTitle = (colKey) => {
|
||||
const col = allColumns.value.find((c) => (c.dataIndex || c.key) === colKey)
|
||||
return col ? col.title : colKey
|
||||
}
|
||||
|
||||
// 初始化可见列和排序
|
||||
watch(
|
||||
() => props.columns,
|
||||
(newColumns) => {
|
||||
const columnKeys = newColumns.filter((col) => col.dataIndex && col.dataIndex !== '_index').map((col) => col.dataIndex || col.key)
|
||||
|
||||
// 如果是首次初始化,使用原始顺序
|
||||
if (sortedColumns.value.length === 0) {
|
||||
sortedColumns.value = [...columnKeys]
|
||||
} else {
|
||||
// 保留已存在的顺序,添加新列
|
||||
const existingKeys = sortedColumns.value.filter((key) => columnKeys.includes(key))
|
||||
const newKeys = columnKeys.filter((key) => !existingKeys.includes(key))
|
||||
sortedColumns.value = [...existingKeys, ...newKeys]
|
||||
}
|
||||
|
||||
visibleColumns.value = [...sortedColumns.value]
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// 切换列的显示状态
|
||||
const toggleColumn = (colKey, checked) => {
|
||||
if (checked) {
|
||||
if (!visibleColumns.value.includes(colKey)) {
|
||||
visibleColumns.value.push(colKey)
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = visibleColumns.value.filter((key) => key !== colKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽开始
|
||||
const handleDragStart = (index, event) => {
|
||||
draggingIndex.value = index
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', index.toString())
|
||||
}
|
||||
|
||||
// 拖拽经过
|
||||
const handleDragOver = (index, event) => {
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
draggingIndex.value = -1
|
||||
}
|
||||
|
||||
// 拖拽放置
|
||||
const handleDrop = (dropIndex) => {
|
||||
if (draggingIndex.value === dropIndex) return
|
||||
|
||||
const draggedKey = sortedColumns.value[draggingIndex.value]
|
||||
const newColumns = [...sortedColumns.value]
|
||||
|
||||
// 移除被拖拽的项
|
||||
newColumns.splice(draggingIndex.value, 1)
|
||||
|
||||
// 插入到新位置
|
||||
newColumns.splice(dropIndex, 0, draggedKey)
|
||||
|
||||
sortedColumns.value = newColumns
|
||||
draggingIndex.value = -1
|
||||
}
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handlePaginationChange = (page, pageSize) => {
|
||||
emit('paginationChange', { page, pageSize })
|
||||
}
|
||||
|
||||
// 处理表格变化(排序、筛选)
|
||||
const handleTableChange = (filters, sorter, extra) => {
|
||||
emit('change', { filters, sorter, extra })
|
||||
}
|
||||
|
||||
// 处理列宽调整
|
||||
const handleResizeColumn = (width, column) => {
|
||||
emit('resizeColumn', { width, column })
|
||||
}
|
||||
|
||||
// 获取表格序号
|
||||
const getTableIndex = (index) => {
|
||||
const { current = 1, pageSize = 10 } = props.pagination || {}
|
||||
return (current - 1) * pageSize + index + 1
|
||||
}
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = computed(() => {
|
||||
let columns = []
|
||||
|
||||
// 添加序号列
|
||||
if (props.showIndex) {
|
||||
columns.push({
|
||||
title: props.indexTitle,
|
||||
dataIndex: '_index',
|
||||
key: '_index',
|
||||
width: props.indexColumnWidth,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
})
|
||||
}
|
||||
|
||||
// 添加数据列(按排序顺序)
|
||||
sortedColumns.value.forEach((colKey) => {
|
||||
// 过滤掉未显示的列
|
||||
if (!visibleColumns.value.includes(colKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
const col = props.columns.find((c) => (c.dataIndex || c.key) === colKey)
|
||||
if (col) {
|
||||
columns.push({
|
||||
...col,
|
||||
customRender: col.slot ? undefined : col.customRender,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
refresh: handleRefresh,
|
||||
getTableIndex,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sc-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
&-tool {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.tool-left,
|
||||
.tool-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.column-setting {
|
||||
min-width: 200px;
|
||||
|
||||
&-header {
|
||||
padding: 8px 0;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 4px;
|
||||
cursor: move;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.5;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-right: 8px;
|
||||
color: #999;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-checkbox-wrapper) {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-setting {
|
||||
min-width: 200px;
|
||||
|
||||
&-header {
|
||||
padding: 8px 0;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&-body {
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="file-upload">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
:custom-request="customUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:accept="accept"
|
||||
:max-count="maxCount"
|
||||
:disabled="disabled"
|
||||
:multiple="multiple"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<a-button v-if="!disabled">
|
||||
<upload-outlined />
|
||||
上传文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UploadOutlined } from '@ant-design/icons-vue'
|
||||
import uploadConfig from '@/config/upload'
|
||||
|
||||
const props = defineProps({
|
||||
// 文件列表
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
default: () => []
|
||||
},
|
||||
// 最大上传数量,默认1为单文件上传
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 接受的文件类型,例如 '.pdf,.doc,.docx' 或 '*'
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否支持多选
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否返回URL字符串(单文件)或URL数组(多文件)
|
||||
returnUrl: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'remove'])
|
||||
|
||||
// 文件列表
|
||||
const fileList = ref([])
|
||||
|
||||
// 初始化文件列表
|
||||
const initFileList = () => {
|
||||
if (props.modelValue) {
|
||||
if (typeof props.modelValue === 'string') {
|
||||
// 单文件上传,字符串格式
|
||||
fileList.value = props.modelValue
|
||||
? [
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'file',
|
||||
status: 'done',
|
||||
url: props.modelValue,
|
||||
response: {
|
||||
src: props.modelValue
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
} else if (Array.isArray(props.modelValue)) {
|
||||
// 多文件上传,数组格式
|
||||
fileList.value = props.modelValue.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: `file${index}`,
|
||||
status: 'done',
|
||||
url: url,
|
||||
response: {
|
||||
src: url
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
fileList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
initFileList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 自定义上传
|
||||
const customUpload = (options) => {
|
||||
const { file, onProgress, onSuccess, onError } = options
|
||||
const formData = new FormData()
|
||||
formData.append(uploadConfig.filename || 'file', file)
|
||||
|
||||
// 使用文件上传API对象
|
||||
const apiObj = uploadConfig.apiObjFile || uploadConfig.apiObj
|
||||
|
||||
apiObj(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
onProgress({ percent }, file)
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
const data = uploadConfig.parseData(res)
|
||||
if (data.code === uploadConfig.successCode) {
|
||||
onSuccess(data, file)
|
||||
message.success('上传成功')
|
||||
} else {
|
||||
onError(new Error(data.msg || '上传失败'))
|
||||
message.error(data.msg || '上传失败')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
onError(error)
|
||||
message.error('上传失败:' + error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = (file) => {
|
||||
const maxSizeMB = uploadConfig.maxSizeFile || uploadConfig.maxSize || 10
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024
|
||||
|
||||
if (file.size > maxSizeBytes) {
|
||||
message.error(`文件大小不能超过 ${maxSizeMB}MB`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 处理文件列表变化
|
||||
const handleChange = ({ fileList: newFileList }) => {
|
||||
fileList.value = newFileList
|
||||
|
||||
// 提取成功的文件URL
|
||||
const successFiles = newFileList
|
||||
.filter((file) => file.status === 'done' && (file.url || file.response?.src))
|
||||
.map((file) => file.url || file.response?.src)
|
||||
|
||||
// 触发更新事件
|
||||
if (props.returnUrl) {
|
||||
// 返回URL字符串或数组
|
||||
const value = props.maxCount === 1 ? successFiles[0] || '' : successFiles
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value, newFileList)
|
||||
} else {
|
||||
// 返回完整文件列表
|
||||
emit('update:modelValue', newFileList)
|
||||
emit('change', newFileList)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件移除
|
||||
const handleRemove = (file) => {
|
||||
emit('remove', file)
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-upload {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,383 +0,0 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:custom-request="customUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:accept="accept"
|
||||
:max-count="maxCount"
|
||||
:disabled="disabled"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: !disabled }"
|
||||
@preview="handlePreview"
|
||||
@change="handleChange"
|
||||
@drop="handleDrop"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
class="custom-upload"
|
||||
:class="{ 'drag-over': isDragOver }"
|
||||
>
|
||||
<div v-if="fileList.length < maxCount && !disabled" class="upload-area">
|
||||
<loading-outlined v-if="uploading" class="upload-icon" />
|
||||
<plus-outlined v-else class="upload-icon" />
|
||||
<div class="ant-upload-text">{{ uploading ? '上传中...' : uploadText }}</div>
|
||||
<div v-if="tip" class="ant-upload-tip">{{ tip }}</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<a-modal
|
||||
:open="previewVisible"
|
||||
:title="previewTitle"
|
||||
:footer="null"
|
||||
:width="800"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<img alt="图片预览" style="width: 100%; max-height: 600px; object-fit: contain;" :src="previewImage" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import uploadConfig from '@/config/upload'
|
||||
|
||||
const props = defineProps({
|
||||
// 图片列表
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
default: () => []
|
||||
},
|
||||
// 最大上传数量,默认1为单图上传
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 接受的文件类型
|
||||
accept: {
|
||||
type: String,
|
||||
default: 'image/*'
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否返回URL字符串(单图)或URL数组(多图)
|
||||
returnUrl: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 上传按钮文字
|
||||
uploadText: {
|
||||
type: String,
|
||||
default: '上传图片'
|
||||
},
|
||||
// 提示文字
|
||||
tip: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 最小宽度(像素)
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大宽度(像素)
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最小高度(像素)
|
||||
minHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大高度(像素)
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 是否删除前确认
|
||||
confirmBeforeRemove: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 自定义上传按钮内容
|
||||
customUploadBtn: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'preview', 'remove', 'uploadSuccess', 'uploadError'])
|
||||
|
||||
// 文件列表
|
||||
const fileList = ref([])
|
||||
|
||||
// 预览相关
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
const previewTitle = computed(() => {
|
||||
return previewImage.value ? '图片预览' : ''
|
||||
})
|
||||
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
|
||||
// 拖拽状态
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// 初始化文件列表
|
||||
const initFileList = () => {
|
||||
if (props.modelValue) {
|
||||
if (typeof props.modelValue === 'string') {
|
||||
// 单图上传,字符串格式
|
||||
fileList.value = props.modelValue
|
||||
? [
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'image.png',
|
||||
status: 'done',
|
||||
url: props.modelValue
|
||||
}
|
||||
]
|
||||
: []
|
||||
} else if (Array.isArray(props.modelValue)) {
|
||||
// 多图上传,数组格式
|
||||
fileList.value = props.modelValue.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: `image${index}.png`,
|
||||
status: 'done',
|
||||
url: url
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
fileList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
initFileList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 自定义上传
|
||||
const customUpload = (options) => {
|
||||
const { file, onProgress, onSuccess, onError } = options
|
||||
const formData = new FormData()
|
||||
formData.append(uploadConfig.filename || 'file', file)
|
||||
|
||||
uploading.value = true
|
||||
|
||||
uploadConfig.apiObj(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
onProgress({ percent }, file)
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
const data = uploadConfig.parseData(res)
|
||||
if (data.code === uploadConfig.successCode) {
|
||||
onSuccess(data, file)
|
||||
message.success('上传成功')
|
||||
emit('uploadSuccess', data, file)
|
||||
} else {
|
||||
onError(new Error(data.msg || '上传失败'))
|
||||
message.error(data.msg || '上传失败')
|
||||
emit('uploadError', data.msg || '上传失败', file)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
onError(error)
|
||||
message.error('上传失败:' + error.message)
|
||||
emit('uploadError', error.message, file)
|
||||
})
|
||||
.finally(() => {
|
||||
uploading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = async (file) => {
|
||||
// 文件大小校验
|
||||
const maxSizeMB = uploadConfig.maxSize || 10
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024
|
||||
|
||||
if (file.size > maxSizeBytes) {
|
||||
message.error(`图片大小不能超过 ${maxSizeMB}MB`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 图片尺寸校验
|
||||
if (props.minWidth || props.maxWidth || props.minHeight || props.maxHeight) {
|
||||
try {
|
||||
const dimensions = await getImageDimensions(file)
|
||||
const { width, height } = dimensions
|
||||
|
||||
if (props.minWidth && width < props.minWidth) {
|
||||
message.error(`图片宽度不能小于 ${props.minWidth}px`)
|
||||
return false
|
||||
}
|
||||
if (props.maxWidth && width > props.maxWidth) {
|
||||
message.error(`图片宽度不能大于 ${props.maxWidth}px`)
|
||||
return false
|
||||
}
|
||||
if (props.minHeight && height < props.minHeight) {
|
||||
message.error(`图片高度不能小于 ${props.minHeight}px`)
|
||||
return false
|
||||
}
|
||||
if (props.maxHeight && height > props.maxHeight) {
|
||||
message.error(`图片高度不能大于 ${props.maxHeight}px`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('图片尺寸校验失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
const getImageDimensions = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.src = e.target.result
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height })
|
||||
}
|
||||
img.onerror = reject
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理预览
|
||||
const handlePreview = async (file) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj)
|
||||
}
|
||||
previewImage.value = file.url || file.preview
|
||||
previewVisible.value = true
|
||||
emit('preview', file)
|
||||
}
|
||||
|
||||
// 获取Base64
|
||||
const getBase64 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理文件列表变化
|
||||
const handleChange = ({ fileList: newFileList }) => {
|
||||
// 更新文件列表,确保上传成功的文件有正确的 url
|
||||
const updatedFileList = newFileList.map((file) => {
|
||||
// 如果文件上传成功且有响应数据但没有 url,则设置 url
|
||||
if (file.status === 'done' && file.response?.src && !file.url) {
|
||||
return {
|
||||
...file,
|
||||
url: file.response.src
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
|
||||
fileList.value = updatedFileList
|
||||
|
||||
// 过滤掉失败的文件
|
||||
const validFileList = updatedFileList.filter((file) => file.status !== 'error')
|
||||
|
||||
// 提取成功的文件URL
|
||||
const successFiles = validFileList
|
||||
.filter((file) => file.status === 'done' && (file.url || file.response?.src))
|
||||
.map((file) => file.url || file.response?.src)
|
||||
|
||||
// 触发更新事件
|
||||
if (props.returnUrl) {
|
||||
// 返回URL字符串或数组
|
||||
const value = props.maxCount === 1 ? successFiles[0] || '' : successFiles
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value, validFileList)
|
||||
} else {
|
||||
// 返回完整文件列表
|
||||
emit('update:modelValue', validFileList)
|
||||
emit('change', validFileList)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽相关
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
// 取消预览
|
||||
const handleCancel = () => {
|
||||
previewVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 24px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.ant-upload-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
border: 2px dashed #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
|
||||
.drag-over .upload-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user