首次提交

This commit is contained in:
2023-06-04 20:01:58 +08:00
parent 00c64c53bb
commit 587f078d21
560 changed files with 106725 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { use } from "echarts/core"
import { CanvasRenderer } from "echarts/renderers"
import { BarChart, LineChart, PieChart, RadarChart, GaugeChart } from "echarts/charts"
import {
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
GraphicComponent
} from "echarts/components"
import MaCrud from "./ma-crud/index.vue"
import MaForm from "./ma-form/index.vue"
import MaChart from "./ma-charts/index.vue"
import MaUpload from "./ma-upload/index.vue"
import MaTreeSlider from "./ma-treeSlider/index.vue"
import MaResource from "./ma-resource/index.vue"
import MaResourceButton from "./ma-resource/button.vue"
import MaUser from "./ma-user/index.vue"
import MaEditor from "./ma-editor/index.vue"
import MaIcon from "./ma-icon/index.vue"
import MaCodeEditor from "./ma-codeEditor/index.vue"
import MaUserInfo from "./ma-userInfo/index.vue"
import MaCityLinkage from "./ma-cityLinkage/index.vue"
use([
CanvasRenderer,
BarChart,
LineChart,
PieChart,
RadarChart,
GaugeChart,
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
GraphicComponent
])
export default {
install(Vue) {
Vue.component("MaChart", MaChart)
Vue.component("MaCrud", MaCrud)
Vue.component("MaForm", MaForm)
Vue.component("MaUpload", MaUpload)
Vue.component("MaTreeSlider", MaTreeSlider)
Vue.component("MaResource", MaResource)
Vue.component("MaResourceButton", MaResourceButton)
Vue.component("MaUser", MaUser)
Vue.component("MaEditor", MaEditor)
Vue.component("MaIcon", MaIcon)
Vue.component("MaCodeEditor", MaCodeEditor)
Vue.component("MaUserInfo", MaUserInfo)
Vue.component("MaCityLinkage", MaCityLinkage)
}
}

View File

@@ -0,0 +1,53 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<v-charts v-if="renderChart" :option="options" :autoresize="autoresize" :style="{ width, height }" />
</template>
<script setup>
import { ref, computed, nextTick } from "vue"
import VCharts from "vue-echarts"
import { useAppStore } from "@/store"
const props = defineProps({
options: {
type: Object,
default() {
return {}
}
},
autoresize: {
type: Boolean,
default: true
},
width: {
type: String,
default: "100%"
},
height: {
type: String,
default: "100%"
}
})
const appStore = useAppStore()
let mode = computed(() => {
return appStore.mode === "dark" ? "dark" : "auto"
})
const renderChart = ref(false)
nextTick(() => {
renderChart.value = true
})
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,170 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-cascader
v-if="props.type === 'cascader'"
v-model="val"
:field-names="props.mode == 'name' ? { value: 'name', label: 'name' } : { value: 'code', label: 'name' }"
:options="jsonData"
allow-search
check-strictly
expand-trigger="hover"
path-mode
placeholder="请选择省市区"
/>
<a-space v-else>
<a-select
v-model="selectData.province"
:field-names="props.mode == 'name' ? { value: 'name', label: 'name' } : { value: 'code', label: 'name' }"
:options="province"
:style="{ width: '220px' }"
allow-clear
allow-search
placeholder="请选择省/直辖市/自治区"
@change="provinceChange"
@clear="
() => {
selectData.city = []
selectData.area = []
selectData.province = []
selectData.city = []
selectData.area = []
province.value = []
}
"
/>
<a-select
v-model="selectData.city"
:field-names="props.mode == 'name' ? { value: 'name', label: 'name' } : { value: 'code', label: 'name' }"
:options="city"
:style="{ width: '220px' }"
allow-clear
allow-search
placeholder="请选择地级市/市辖区"
@change="cityChange"
@clear="
() => {
selectData.city = []
selectData.area = []
selectData.city = []
selectData.area = []
}
"
/>
<a-select
v-model="selectData.area"
:field-names="props.mode == 'name' ? { value: 'name', label: 'name' } : { value: 'code', label: 'name' }"
:options="area"
:style="{ width: '220px' }"
allow-clear
allow-search
placeholder="请选择区县"
@clear="
() => {
selectData.area = []
selectData.area = []
}
"
/>
</a-space>
</template>
<script setup>
import jsonData from "./lib/city.json"
import { ref, watch } from "vue"
import { isObject } from "lodash"
const val = ref()
const selectData = ref({ province: [], city: [], area: [] })
const province = ref([])
const city = ref([])
const area = ref([])
const emit = defineEmits(["update:modelValue"])
const props = defineProps({
modelValue: [Number, String, Object],
type: { type: String, default: "select" },
mode: { type: String, default: "name" }
})
if (props.type === "select") {
province.value = jsonData.map((item) => {
return { code: item.code, name: item.name }
})
}
const provinceChange = (val, clear = true) => {
if (clear) {
selectData.value.city = []
selectData.value.area = []
area.value = []
city.value = []
}
jsonData.map((item) => {
if (props.mode == "name" && val == item.name) {
city.value = item.children
}
if (props.mode == "code" && val == item.code) {
city.value = item.children
}
})
}
const cityChange = (val, clear = true) => {
if (clear) {
selectData.value.area = []
area.value = []
}
city.value.map((item) => {
if (props.mode == "name" && val == item.name) {
area.value = item.children
}
if (props.mode == "code" && val == item.code) {
area.value = item.children
}
})
}
const setSelectData = () => {
if (props.type === "select") {
if (val.value && isObject(val.value)) {
selectData.value.province = val.value.province ? val.value.province : ""
selectData.value.city = val.value.city ? val.value.city : ""
selectData.value.area = val.value.area ? val.value.area : ""
selectData.value.province && provinceChange(selectData.value.province, false)
selectData.value.city && selectData.value.province && cityChange(selectData.value.city, false)
}
}
}
val.value = props.modelValue
watch(
() => props.modelValue,
(vl) => () => {
val.value = vl
setSelectData()
},
{ deep: true }
)
watch(
() => val.value,
(vl) => emit("update:modelValue", vl)
)
watch(
() => selectData.value,
(vl) => emit("update:modelValue", vl),
{ deep: true }
)
setSelectData()
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="editor" ref="dom" :style="'width: 100%; height: ' + props.height + 'px'"></div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue"
import { useAppStore } from "@/store"
import { formatJson } from "@/utils/common"
import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"
import "monaco-editor/esm/vs/basic-languages/php/php.contribution"
import "monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution"
import "monaco-editor/esm/vs/basic-languages/html/html.contribution"
import "monaco-editor/esm/vs/basic-languages/css/css.contribution"
import "monaco-editor/esm/vs/editor/contrib/find/browser/findController"
const appStore = useAppStore()
const props = defineProps({
modelValue: {
type: [String, Object, Array],
default: () => ""
},
isBind: {
type: Boolean,
default: false
},
height: {
type: Number,
default: 400
},
language: {
type: String,
default: "javascript"
},
readonly: {
type: Boolean,
default: false
}
})
const emit = defineEmits(["update:modelValue"])
const dom = ref()
let instance
onMounted(() => {
instance = monaco.editor.create(dom.value, {
value: typeof props.modelValue === "string" ? props.modelValue : formatJson(props.modelValue),
tabSize: 2,
automaticLayout: true,
scrollBeyondLastLine: false,
language: props.language,
theme: appStore.mode === "light" ? "vs" : "vs-dark",
autoIndent: true,
minimap: { enabled: false },
readOnly: props.readonly,
folding: true,
acceptSuggestionOnCommitCharacter: true,
acceptSuggestionOnEnter: true,
contextmenu: true
})
instance.onDidChangeModelContent(() => {
emit("update:modelValue", instance.getValue())
})
})
if (props.isBind) {
watch(
() => props.modelValue,
(vl) => instance.setValue(typeof vl === "string" ? vl : formatJson(vl))
)
}
</script>
<style scoped lang="less">
.editor {
border: 1px solid var(--color-border-2);
border-radius: 3px;
background: var(--color-bg-2);
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<a-input-group class="w-full">
<a-trigger position="bottom" trigger="click" auto-fit-position :unmount-on-close="false">
<a-button type="primary">选择颜色</a-button>
<template #content>
<ColorPicker
theme="dark"
:color="val"
:sucker-hide="true"
:colors-default="defaultColorList"
@changeColor="selectColor"
style="width: 218px"
/>
</template>
</a-trigger>
<a-input v-model="val" :style="`color: ${val}`" readonly :placeholder="props.placeholder"> </a-input>
<a-tooltip content="复制">
<a-button @click="copyColor"
><template #icon><icon-copy class="cursor-pointer" /></template
></a-button>
</a-tooltip>
</a-input-group>
</template>
<script setup>
import { ref, watch, reactive } from "vue"
import { ColorPicker } from "vue-color-kit"
import "vue-color-kit/dist/vue-color-kit.css"
import { generate, getRgbStr } from "@arco-design/color"
import useClipboard from "vue-clipboard3"
import { Message } from "@arco-design/web-vue"
const val = ref()
const props = defineProps({
modelValue: String,
placeholder: { type: String, default: "请选择颜色" }
})
const selectColor = (color) => {
val.value = color.hex
}
const copyColor = async () => {
try {
await useClipboard().toClipboard(val.value)
Message.success("复制成功")
} catch (e) {
Message.error("复制失败")
}
}
const defaultColorList = reactive([
"#165DFF",
"#F53F3F",
"#F77234",
"#F7BA1E",
"#00B42A",
"#14C9C9",
"#3491FA",
"#722ED1",
"#F5319D",
"#D91AD9",
"#34C759",
"#43a047",
"#7cb342",
"#c0ca33",
"#86909c",
"#6d4c41"
])
const emit = defineEmits(["update:modelValue"])
watch(
() => val.value,
(vl) => emit("update:modelValue", vl)
)
</script>

View File

@@ -0,0 +1,299 @@
<template>
<template v-for="row in props.columns" :key="row[options.pk]">
<template v-if="!row.hide">
<a-table-column
:title="row.title"
:width="row.width"
:ellipsis="row.ellipsis ?? true"
:filterable="row.filterable"
:cell-class="row.cellClass"
:header-cell-class="row.headerCellClass"
:body-cell-class="row.bodyCellClass"
:summary-cell-class="row.summaryCellClass"
:cell-style="row.cellStyle"
:header-cell-style="row.headerCellStyle"
:body-cell-style="row.bodyCellStyle"
:summary-cell-style="row.summaryCellStyle"
:tooltip="row.tooltip ?? true"
:align="row.align || 'left'"
:fixed="row.fixed"
v-if="row.children && row.children.length > 0"
>
<column
@refresh="() => refresh()"
:isRecovery="props.isRecovery"
:crudFormRef="props.crudFormRef"
:columns="row.children"
>
<template
v-for="(childRow, childIndex) in row.children"
:key="childIndex"
#[childRow.dataIndex]="{ record, column, rowIndex }"
>
<slot :name="`${childRow.dataIndex}`" v-bind="{ record, column, rowIndex }" />
</template>
</column>
</a-table-column>
<a-table-column
:title="row.title"
:data-index="row.dataIndex"
:width="row.width"
:ellipsis="row.ellipsis ?? true"
:filterable="row.filterable"
:cell-class="row.cellClass"
:header-cell-class="row.headerCellClass"
:body-cell-class="row.bodyCellClass"
:summary-cell-class="row.summaryCellClass"
:cell-style="row.cellStyle"
:header-cell-style="row.headerCellStyle"
:body-cell-style="row.bodyCellStyle"
:summary-cell-style="row.summaryCellStyle"
:tooltip="row.dataIndex === '__operation' ? false : row.tooltip ?? true"
:align="row.align || 'left'"
:fixed="row.fixed"
:sortable="row.sortable"
v-else
>
<template #cell="{ record, column, rowIndex }">
<!-- 操作栏 -->
<template v-if="row.dataIndex === '__operation'">
<a-scrollbar type="track" style="overflow: auto">
<a-space size="mini">
<slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
<slot name="operationCell" v-bind="{ record, column, rowIndex }">
<!-- <a-link
v-if="
options.see.show
&& ($common.auth(options.see.auth || [])
|| (options.see.role || []))
"
type="primary"
><icon-eye /> {{ options.see.text || '查看' }}</a-link> -->
<a-link
v-if="
(isFunction(options.edit.show)
? options.edit.show(record)
: options.edit.show) && !props.isRecovery
"
v-auth="options.edit.auth || []"
v-role="options.edit.role || []"
type="primary"
@click="editAction(record)"
>
<icon-edit /> {{ options.edit.text || "编辑" }}
</a-link>
<a-popconfirm
content="确定要恢复该数据吗?"
position="bottom"
@ok="recoveryAction(record)"
v-if="
(isFunction(options.recovery.show)
? options.recovery.show(record)
: options.recovery.show) && props.isRecovery
"
v-auth="options.recovery.auth || []"
v-role="options.recovery.role || []"
>
<a-link type="primary"
><icon-undo /> {{ options.recovery.text || "恢复" }}
</a-link>
</a-popconfirm>
<a-popconfirm
content="确定要删除该数据吗?"
position="bottom"
@ok="deleteAction(record)"
v-if="
isFunction(options.delete.show)
? options.delete.show(record)
: options.delete.show
"
>
<a-link
type="primary"
v-auth="options.delete.auth || []"
v-role="options.delete.role || []"
>
<icon-delete />
{{
props.isRecovery
? options.delete.realText || "删除"
: options.delete.text || "删除"
}}
</a-link>
</a-popconfirm>
</slot>
<slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
</a-space>
</a-scrollbar>
</template>
<template v-else-if="row.customRender">
<custom-render
:column="column"
:record="record"
:render="row.customRender"
:rowIndex="rowIndex"
></custom-render>
</template>
<slot :name="row.dataIndex" v-bind="{ record, column, rowIndex }" v-else>
<template v-if="row.dataIndex === '__index'">{{ getIndex(rowIndex) }}</template>
<template v-if="row.dict && row.dict.translation">
<template v-if="isArray(get(record, row.dataIndex))">
<a-tag v-for="item in get(record, row.dataIndex)" class="ml-1">{{
getDataIndex(row, item)
}}</a-tag>
</template>
<a-tag v-else-if="row.dict.tagColors" :color="getTagColor(row, record)">
{{ getDataIndex(row, record) }}
</a-tag>
<a-tag v-else-if="row.dict.tagColor" :color="row.dict.tagColor">{{
getDataIndex(row, record)
}}</a-tag>
<span v-else>{{ getDataIndex(row, record) }}</span>
</template>
<template v-else-if="row.dataIndex && row.dataIndex.indexOf('.') !== -1">
{{ get(record, row.dataIndex) }}
</template>
<template v-else-if="row.formType === 'upload'">
<a-link @click="imageSee(row, record, row.dataIndex)"><icon-image /> 查看图片</a-link>
</template>
<template v-else>{{ record[row.dataIndex] }}</template>
</slot>
</template>
</a-table-column>
</template>
</template>
</template>
<script setup>
import { inject } from "vue"
import config from "@/config/crud"
import uploadConfig from "@/config/upload"
import { Message } from "@arco-design/web-vue"
import { isFunction, get, isArray, isObject } from "lodash"
import CustomRender from "../js/custom-render"
import tool from "@/utils/tool"
import commonApi from "@/api/common"
const emit = defineEmits(["refresh", "showImage"])
const props = defineProps({
columns: Array,
isRecovery: Boolean,
crudFormRef: Object
})
const options = inject("options")
const requestParams = inject("requestParams")
const dictTrans = inject("dictTrans")
const dictColors = inject("dictColors")
const imageSee = async (row, record, dataIndex) => {
if (row.returnType) {
if (row.returnType === "url") {
emit("showImage", record[dataIndex])
return
}
if (!["id", "hash"].includes(row.returnType)) {
Message.info("该图片无法查看")
return
}
Message.info("获取图片中,请稍等...")
const res =
row.returnType === "id"
? await commonApi.getFileInfoById({ id: record.id })
: await commonApi.getFileInfoByHash({ hash: record.hash })
const result = res?.success ?? false
if (!result) {
Message.info("图片信息无法获取")
return
}
const isImage = res.data.mime_type.indexOf("image") > -1
result &&
emit(
"showImage",
isImage
? tool.attachUrl(res.data.url, uploadConfig.storageMode[res.data.storage_mode])
: "not-image.png"
)
} else {
if (!record[row.dataIndex]) {
Message.info("无图片")
return
}
emit("showImage", record[row.dataIndex] ?? "not-image.png")
}
}
const getTagColor = (row, record) => {
return dictColors(
row.dataIndex,
row.dataIndex.indexOf(".") > -1 ? get(record, row.dataIndex) : record[row.dataIndex]
)
}
const getDataIndex = (row, record) => {
if (isObject(record)) {
return dictTrans(
row.dataIndex,
row.dataIndex.indexOf(".") > -1 ? get(record, row.dataIndex) : record[row.dataIndex]
)
} else {
return dictTrans(row.dataIndex, record)
}
}
const getIndex = (rowIndex) => {
const index = rowIndex + 1
if (requestParams[config.request.page] === 1) {
return index
} else {
return (requestParams[config.request.page] - 1) * requestParams[config.request.pageSize] + index
}
}
const editAction = (record) => {
isFunction(options.beforeOpenEdit) && options.beforeOpenEdit(record)
if (options.edit.action && isFunction(options.edit.action)) {
options.edit.action(record)
} else {
props.crudFormRef.edit(record)
}
}
const recoveryAction = async (record) => {
const response = await options.recovery.api({ ids: [record[options.pk]] })
response.success && Message.success(response.message || `恢复成功!`)
emit("refresh")
}
const deleteAction = async (record) => {
let data = {}
if (isFunction(options.beforeDelete) && !(data = options.beforeDelete([record[options.pk]]))) {
return false
}
const api = props.isRecovery ? options.delete.realApi : options.delete.api
const response = await api(Object.assign({ ids: [record[options.pk]] }, data))
if (options.afterDelete && isFunction(options.afterDelete)) {
options.afterDelete(response, record)
}
response.success && Message.success(response.message || `删除成功!`)
emit("refresh")
}
const refresh = () => {
emit("refresh")
}
defineExpose({ deleteAction, recoveryAction })
</script>
<style scoped>
:deep(.arco-image-img) {
object-fit: contain;
background-color: var(--color-fill-4);
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<ul
class="ma-crud-contextmenu shadow-lg"
v-show="crudContextMenuVisible"
:style="{ left: left + 'px', top: top + 'px', height: 'auto' }"
>
<template v-for="item in options?.contextMenu?.items ?? []">
<li v-if="item.operation === 'divider'"><a-divider margin="8px" /></li>
<li v-if="item.operation === 'print'">
<div class="context-menu-item" @click="execCommand(item)">
<icon-printer /> <span class="ml-2">{{ item.text ?? "打印表格" }}</span>
</div>
</li>
<li v-if="item.operation === 'refresh'">
<div class="context-menu-item" @click="execCommand(item)">
<icon-refresh /> <span class="ml-2">{{ item.text ?? "刷新表格" }}</span>
</div>
</li>
<li v-if="item.operation === 'add' && showVerify('add')">
<div class="context-menu-item" @click="execCommand(item)">
<icon-plus /> <span class="ml-2">{{ item.text ?? "新增数据" }}</span>
</div>
</li>
<li v-if="item.operation === 'edit' && showVerify('edit')">
<div class="context-menu-item" @click="execCommand(item)">
<icon-edit /> <span class="ml-2">{{ item.text ?? "编辑数据" }}</span>
</div>
</li>
<li v-if="item.operation === 'delete' && showVerify('delete')">
<a-popconfirm content="确实要删除此数据吗?" @ok="execCommand(item)">
<div class="context-menu-item">
<icon-delete /> <span class="ml-2">{{ item.text ?? "删除数据" }}</span>
</div>
</a-popconfirm>
</li>
<li v-if="!defaultOperation.includes(item.operation)">
<div class="context-menu-item" @click="execCommand(item)">
<component v-if="item.icon" :is="getIcon(item)" />
<span class="ml-2">{{ getText(item) }}</span>
</div>
</li>
</template>
</ul>
</template>
<script setup>
import { ref, inject, watch, nextTick } from "vue"
import checkAuth from "@/directives/auth/auth"
import { isArray } from "lodash"
const left = ref(0)
const top = ref(0)
const crudContextMenuVisible = ref(false)
const currentRow = ref()
const emit = defineEmits(["execCommand"])
const options = inject("options")
const isRecovery = inject("isRecovery")
const defaultOperation = ["refresh", "print", "edit", "divider", "delete", "add", "nextPage", "prevPage"]
const showVerify = (type) => {
if (!options[type].show) {
return false
}
if (isRecovery.value === true) {
return false
}
const authList = options[type].auth
if (isArray(authList)) {
for (let index in authList) {
if (!checkAuth(authList[index])) {
return false
}
}
}
return true
}
watch(
() => crudContextMenuVisible.value,
(value) => {
const crudContextMenuhandler = (e) => {
const dom = document.querySelector(".ma-crud-contextmenu")
if (dom && !dom.contains(e.target)) {
closeCrudcontextMenu()
}
}
value
? document.body.addEventListener("click", (e) => crudContextMenuhandler(e))
: document.body.removeEventListener("click", (e) => crudContextMenuhandler(e))
}
)
const openContextMenu = async (ev, record) => {
crudContextMenuVisible.value = true
currentRow.value = record
await nextTick(() => {
const domHeight = document.querySelector(".ma-crud-contextmenu").offsetHeight
if (document.body.offsetHeight - ev.pageY < domHeight) {
top.value = ev.clientY - domHeight
} else {
top.value = ev.clientY
}
left.value = ev.clientX
})
}
const closeCrudcontextMenu = () => {
currentRow.value = null
crudContextMenuVisible.value = false
}
const getIcon = (item) => {
if (!defaultOperation.includes(item.operation)) {
return item.icon
}
}
const getText = (item) => {
if (!defaultOperation.includes(item.operation)) {
return item.text
}
}
const execCommand = (item) => {
emit("execCommand", { contextItem: item, record: currentRow.value })
crudContextMenuVisible.value = false
}
defineExpose({
openContextMenu,
closeCrudcontextMenu
})
</script>
<style scoped lang="less">
.ma-crud-contextmenu {
position: fixed;
min-width: 200px;
z-index: 999;
background: var(--color-bg-2);
border: 1px solid var(--color-border-2);
padding: 7px 0;
border-radius: 4px;
li .context-menu-item {
cursor: pointer;
padding: 5px 10px;
}
li:hover {
background-color: var(--color-primary-light-1);
}
}
</style>

View File

@@ -0,0 +1,350 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<component
:is="componentName"
v-model:visible="dataVisible"
:on-before-ok="submit"
@cancel="close"
ok-text="保存"
cancel-text="关闭"
draggable
:width="options.formOption.width"
:fullscreen="options.formOption.isFull || false"
unmount-on-close
>
<template #title>{{ actionTitle }}</template>
<a-spin :loading="dataLoading" tip="加载中..." class="w-full">
<ma-form v-model="form" :columns="formColumns" :options="{ showButtons: false }" ref="maFormRef">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-form>
</a-spin>
</component>
</template>
<script setup>
import { ref, toRaw, getCurrentInstance, inject, provide } from "vue"
import { Message } from "@arco-design/web-vue"
import { containerItems } from "@cps/ma-form/js/utils"
import { isArray, isFunction, get, cloneDeep, isUndefined } from "lodash"
import { useRouter } from "vue-router"
import tool from "@/utils/tool"
import { useFormStore } from "@/store/index"
const formStore = useFormStore()
const router = useRouter()
const app = getCurrentInstance().appContext.app
const maFormRef = ref()
const componentName = ref("a-modal")
const columns = inject("columns")
const layoutColumns = ref(new Map())
const options = inject("options")
const formColumns = ref([])
const currentAction = ref("")
const dataVisible = ref(false)
const form = ref({})
const actionTitle = ref("")
const dataLoading = ref(true)
const emit = defineEmits(["success", "error"])
provide("form", toRaw(form))
const submit = async () => {
const formData = maFormRef.value.getFormData()
if (await maFormRef.value.validateForm()) {
return false
}
let response
if (currentAction.value === "add") {
isFunction(options.beforeAdd) && (await options.beforeAdd(formData))
response = await options.add.api(formData)
isFunction(options.afterAdd) && (await options.afterAdd(response, formData))
} else {
isFunction(options.beforeEdit) && (await options.beforeEdit(formData))
response = await options.edit.api(formData[options.pk], formData)
isFunction(options.afterEdit) && (await options.afterEdit(response, formData))
}
if (response.success) {
Message.success(response.message || `${actionTitle.value}成功!`)
emit("success", response)
return true
} else if (response.success === false && (typeof response.code === "undefined" || response.code !== 200)) {
Message.error(response.message || `${actionTitle.value}失败!`)
return false
}
}
const open = () => {
formColumns.value = []
layoutColumns.value = new Map()
init()
if (options.formOption.viewType === "tag") {
if (!options.formOption.tagId) {
Message.info("未配置 tagId")
return
}
if (!options.formOption.tagName) {
Message.info("未配置 tagName")
return
}
const config = {
options,
formColumns: formColumns.value
}
formStore.crudList[options.id] = false
const queryParams = {
tagId: options.formOption.tagId,
op: currentAction.value
}
queryParams.key = form.value[options.formOption?.titleDataIndex ?? ""] ?? form.value[options.pk]
if (formStore.formList[options.formOption.tagId] === undefined) {
formStore.formList[options.formOption.tagId] = {
config,
addData: {},
editData: {}
}
}
if (currentAction.value === "add") {
formStore.formList[options.formOption.tagId].addData = cloneDeep(form.value)
} else {
formStore.formList[options.formOption.tagId].editData[queryParams.key] = cloneDeep(form.value)
}
form.value = {}
router.push(`/openForm/${options.formOption.tagId}` + tool.httpBuild(queryParams, true))
} else {
componentName.value = options.formOption.viewType === "drawer" ? "a-drawer" : "a-modal"
dataVisible.value = true
}
}
const close = () => {
dataVisible.value = false
formColumns.value = []
form.value = {}
}
const add = () => {
actionTitle.value = "新增"
currentAction.value = "add"
form.value = {}
open()
}
const edit = (data) => {
actionTitle.value = "编辑"
currentAction.value = "edit"
form.value = {}
for (let i in data) form.value[i] = data[i]
open(data)
}
const init = () => {
dataLoading.value = true
const layout = JSON.parse(JSON.stringify(options?.formOption?.layout ?? []))
// const layout = options?.formOption?.layout ?? []
columns.map(async (item) => {
await columnItemHandle(item)
})
// 设置表单布局
settingFormLayout(layout)
if (isArray(layout) && layout.length > 0) {
formColumns.value = layout
const excludeColumns = ["__index", "__operation"]
columns.map((item) => {
if (options.formExcludePk) excludeColumns.push(options.pk)
if (excludeColumns.includes(item.dataIndex)) return
!item.__formLayoutSetting && formColumns.value.push(item)
})
}
dataLoading.value = false
}
const columnItemHandle = async (item) => {
const excludeColumns = ["__index", "__operation"]
if (options.formExcludePk) excludeColumns.push(options.pk)
if (excludeColumns.includes(item.dataIndex)) {
layoutColumns.value.set(item.dataIndex, { dataIndex: item.dataIndex, layoutFormRemove: true })
return
}
layoutColumns.value.set(item.dataIndex, item)
formColumns.value.push(item)
// 针对带点的数据处理
if (item.dataIndex && item.dataIndex.indexOf(".") > -1) {
form.value[item.dataIndex] = get(form.value, item.dataIndex)
}
// add 默认值处理
if (currentAction.value === "add") {
if (item.addDefaultValue && isFunction(item.addDefaultValue)) {
form.value[item.dataIndex] = await item.addDefaultValue(form.value)
} else if (typeof item.addDefaultValue != "undefined") {
form.value[item.dataIndex] = item.addDefaultValue
}
}
// edit 默认值处理
if (currentAction.value === "edit") {
if (item.editDefaultValue && isFunction(item.editDefaultValue)) {
form.value[item.dataIndex] = await item.editDefaultValue(form.value)
} else if (typeof item.editDefaultValue != "undefined") {
form.value[item.dataIndex] = item.editDefaultValue
}
}
// 其他处理
item.display = formItemShow(item)
item.disabled = formItemDisabled(item)
item.readonly = formItemReadonly(item)
item.labelWidth = formItemLabelWidth(item)
item.rules = getRules(item)
}
const settingFormLayout = (layout) => {
if (!isArray(layout)) {
return
}
layout.map(async (item, index) => {
if (containerItems.includes(item.formType)) {
switch (item.formType) {
case "tabs":
if (item.tabs) {
item.tabs.map((tab) => {
tab.formList && (tab.formList = settingFormLayout(tab.formList))
})
}
break
case "card":
item.formList && (item.formList = settingFormLayout(item.formList))
break
case "grid-tailwind":
case "grid":
if (item.cols) {
item.cols.map((col) => {
col.formList && (col.formList = settingFormLayout(col.formList))
})
}
break
case "table":
if (item.rows) {
item.rows.map((row) => {
if (row.cols) {
row.cols.map((col) => {
col.formList && (col.formList = settingFormLayout(col.formList))
})
}
})
}
break
}
} else {
let column = layoutColumns.value.get(item.dataIndex)
// 公共column存在以dataIndex作为判断获取配置项
if (column) {
// 判断是否layout配置移除
if (column.layoutFormRemove) {
layout[index] = undefined
return
}
column["__formLayoutSetting"] = true
item = column
layout[index] = item
} else {
// 当公共column不存在则执行column配置项处理
await columnItemHandle(item)
let column = layoutColumns.value.get(item.dataIndex)
if (column.layoutFormRemove) {
layout[index] = undefined
return
}
item["__formLayoutSetting"] = true
layout[index] = item
}
}
})
// 移除
return layout.filter((item) => {
return !isUndefined(item)
})
}
const formItemShow = (item) => {
if (currentAction.value === "add") {
return item.addDisplay !== false
}
if (currentAction.value === "edit") {
return item.editDisplay !== false
}
return item.display !== false
}
const formItemDisabled = (item) => {
if (currentAction.value === "add" && !isUndefined(item.addDisabled)) {
return item.addDisabled
}
if (currentAction.value === "edit" && !isUndefined(item.editDisabled)) {
return item.editDisabled
}
if (!isUndefined(item.disabled)) {
return item.disabled
}
return false
}
const formItemReadonly = (item) => {
if (currentAction.value === "add" && !isUndefined(item.addReadonly)) {
return item.addReadonly
}
if (currentAction.value === "edit" && !isUndefined(item.editReadonly)) {
return item.editReadonly
}
if (!isUndefined(item.readonly)) {
return item.readonly
}
return false
}
const formItemLabelWidth = (item) => {
return item.labelWidth ?? options.labelWidth ?? undefined
}
const toRules = (rules) => {
if (!rules) {
return []
}
if (isArray(rules)) {
return rules.map((v) => ({ ...v }))
}
if (!rules.validator && isFunction(rules.validatorFormData)) {
rules.validator = (value, cb) => {
rules.validatorFormData(value, cb, form.value)
}
}
return { ...rules }
}
const getRules = (item) => {
if (currentAction.value === "add") {
return toRules(item.addRules ?? item.commonRules ?? [])
}
if (currentAction.value === "edit") {
return toRules(item.editRules ?? item.commonRules ?? [])
}
}
const getFormColumns = async (type = "add") => {
await init()
return formColumns.value
}
defineExpose({ add, edit, currentAction, form, getFormColumns })
</script>

View File

@@ -0,0 +1,69 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-modal v-model:visible="visible" :footer="false" @cancel="close" draggable>
<template #title>导入</template>
<a-upload draggable :custom-request="upload" :show-file-list="false" accept=".xlsx,.xls">
<template #upload-button>
<div
style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)"
class="rounded text-center p-7"
>
<div>
<icon-upload class="text-5xl text-gray-400" />
<div class="text-red-600 font-bold">导入Excel</div>
将文件拖到此处<span style="color: #3370ff">点击上传</span>只能上传 xls/xlsx 文件
</div>
</div>
</template>
</a-upload>
<div class="mt-5 italic text-right"><a-link @click="sendDownload">下载Excel模板</a-link></div>
</a-modal>
</template>
<script setup>
import { ref, inject } from "vue"
import commonApi from "@/api/common"
import tool from "@/utils/tool"
import { Message } from "@arco-design/web-vue"
const visible = ref(false)
const options = inject("options")
const open = () => (visible.value = true)
const close = () => (visible.value = false)
const upload = (fileOption) => {
Message.info("文件上传导入中...")
const dataForm = new FormData()
dataForm.append("file", fileOption.fileItem.file)
commonApi.importExcel(options.import.url, dataForm).then((res) => {
res.success && Message.success(res.message || "导入成功")
close()
})
}
const sendDownload = () => {
Message.info("请求服务器下载文件中...")
const url = options.import.templateUrl
if (/^(http|https)/g.test(url)) {
window.open(url)
} else {
commonApi.download(url).then((res) => {
tool.download(res)
Message.success("请求成功,文件开始下载")
})
}
}
defineExpose({ open, close })
</script>

View File

@@ -0,0 +1,180 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-spin :loading="searchLoading" :tip="options.searchLoadingText" v-if="showSearch">
<a-form
:model="searchForm"
layout="inline"
:label-align="options?.searchLabelAlign"
ref="searchRef"
@submit="handlerSearch"
>
<div :class="[gridClass, options?.searchCustomClass]">
<template v-for="(component, index) in searchColumns" :key="index">
<a-form-item
:field="component.dataIndex"
:label="component.title"
label-col-flex="auto"
:label-col-style="{ width: component.searchLabelWidth ?? options.searchLabelWidth }"
>
<slot :name="`${component.dataIndex}`" v-bind="{ searchForm, component }">
<component :is="getComponentName(component.formType)" :component="component" />
</slot>
</a-form-item>
</template>
</div>
<div class="text-center mt-5 w-full" v-if="searchColumns.length > 0">
<a-space size="medium">
<slot name="searchBeforeButtons" />
<slot name="searchButtons">
<a-button type="primary" html-type="submit">
<template #icon><icon-search /></template>
{{ options.searchSubmitButtonText }}
</a-button>
<a-button @click="resetSearch">
<template #icon><icon-delete /></template>
{{ options.searchResetButtonText }}
</a-button>
</slot>
<slot name="searchAfterButtons" />
</a-space>
</div>
</a-form>
</a-spin>
</template>
<script setup>
import { ref, inject, provide, markRaw } from "vue"
import MaFormInput from "./searchFormItem/form-input.vue"
import MaFormPicker from "./searchFormItem/form-picker.vue"
import MaFormSelect from "./searchFormItem/form-select.vue"
import MaFormCascader from "./searchFormItem/form-cascader.vue"
import MaFormTreeSelect from "./searchFormItem/form-tree-select.vue"
import { cloneDeep, isFunction } from "lodash"
const options = inject("options")
const columns = inject("columns")
const requestParams = inject("requestParams")
const gridClass = ref(["ma-search-grid", "w-full", "grid", "lg:grid-cols-" + options.searchColNumber ?? 4])
const searchLoading = ref(false)
const showSearch = ref(true)
const searchRef = ref(null)
const searchColumns = ref([])
const searchForm = ref({})
provide("searchForm", searchForm)
provide("columns", columns)
const emit = defineEmits(["search"])
if (columns.length > 0) {
searchColumns.value = cloneDeep(
columns.filter((item) => item.search === true && options.tabs?.dataIndex != item.dataIndex)
)
}
const handlerSearch = () => {
emit("search", searchForm.value)
}
const resetSearch = async () => {
searchRef.value.resetFields()
if (options.resetSearch && isFunction(options.resetSearch)) {
await options.resetSearch(searchForm.value)
}
emit("search", searchForm.value)
}
const componentList = ref({
MaFormSelect: markRaw(MaFormSelect),
MaFormPicker: markRaw(MaFormPicker),
MaFormCascader: markRaw(MaFormCascader),
MaFormTreeSelect: markRaw(MaFormTreeSelect),
MaFormInput: markRaw(MaFormInput)
})
const getComponentName = (formType) => {
if (["select", "radio", "checkbox", "transfer"].includes(formType)) {
return componentList.value["MaFormSelect"]
} else if (["date", "month", "year", "week", "quarter", "range", "time"].includes(formType)) {
return componentList.value["MaFormPicker"]
} else if (formType === "cascader") {
return componentList.value["MaFormCascader"]
} else if (formType === "tree-select") {
return componentList.value["MaFormTreeSelect"]
} else {
return componentList.value["MaFormInput"]
}
}
const setSearchHidden = () => (showSearch.value = false)
const setSearchDisplay = () => (showSearch.value = true)
const setSearchLoading = () => (searchLoading.value = true)
const setSearchUnLoading = () => (searchLoading.value = false)
const getSearchFormRef = () => searchRef.value
const getSearchColumns = () => searchColumns.value
defineExpose({
getSearchFormRef,
getSearchColumns,
showSearch,
setSearchHidden,
setSearchDisplay,
setSearchLoading,
setSearchUnLoading
})
</script>
<style scoped lang="less">
:deep(.arco-form-item-label-required-symbol svg) {
vertical-align: baseline !important;
}
@media (min-width: 1024px) {
.lg\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.lg\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.lg\:grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.lg\:grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.lg\:grid-cols-9 {
grid-template-columns: repeat(9, minmax(0, 1fr));
}
.lg\:grid-cols-10 {
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.lg\:grid-cols-11 {
grid-template-columns: repeat(11, minmax(0, 1fr));
}
.lg\:grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
</style>

View File

@@ -0,0 +1,42 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-cascader
v-model="value"
:placeholder="props.component.searchPlaceholder || `请选择${props.component.title}`"
allow-clear
allow-search
:expand-trigger="props.component.trigger || 'click'"
:options="dicts[props.component.dataIndex]"
:multiple="props.component.multiple"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const dicts = inject("dicts")
const value = ref(get(searchForm.value, props.component.dataIndex, props.component.searchDefaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,37 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-input
v-model="value"
:placeholder="props.component.searchPlaceholder ?? `请输入${props.component.title}`"
allow-clear
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const value = ref(get(searchForm.value, props.component.dataIndex, props.component.searchDefaultValue ?? ""))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,61 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<component
:is="getComponentName()"
v-model="value"
:placeholder="
props.component.formType === 'range'
? ['请选择开始时间', '请选择结束时间']
: `请选择${props.component.title}`
"
:time-picker-props="props.component.formType == 'range' ? { defaultValue: ['00:00:00', '23:59:59'] } : {}"
:show-time="props.component.showTime"
:format="props.component.format || ''"
:mode="props.component.mode"
allow-clear
style="width: 100%"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const getComponentName = () => {
if (["date", "month", "year", "week", "quarter", "range", "time"].includes(props.component.formType)) {
return `a-${props.component.formType}-picker`
}
}
let defaultValue
if (props.component.formType === "range") {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,60 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-select
v-model="value"
:virtual-list-props="props.component.virtualListProps"
:placeholder="props.component.searchPlaceholder ?? `请选择${props.component.title}`"
allow-clear
allow-search
:scrollbar="props.component.scrollbar"
:format-label="props.component.formatLabel"
:max-tag-count="1"
:options="dicts[props.component.dataIndex]"
:multiple="props.component.multiple || ['transfer', 'checkbox'].includes(props.component.formType)"
@change="handlerChangeeEvent"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { handlerCascader } from "@cps/ma-form/js/networkRequest"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const columns = inject("columns")
const dicts = inject("dicts")
let defaultValue
if (props.component.multiple === true) {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
const handlerChangeeEvent = (value) => {
handlerCascader(value, props.component, columns, dicts, searchForm.value)
}
</script>

View File

@@ -0,0 +1,52 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-tree-select
v-model="value"
:treeProps="props.component.treeProps"
:placeholder="props.component.searchPlaceholder || `请选择${props.component.title}`"
allow-clear
allow-search
:field-names="props.component.dict.props || { key: 'value', title: 'label' }"
:tree-checkable="props.component.multiple"
:multiple="props.component.multiple"
:data="dicts[props.component.dataIndex]"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const dicts = inject("dicts")
let defaultValue
if (props.component.multiple === true) {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,178 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-drawer :visible="visible" unmountOnClose :footer="false" :width="900" @cancel="onCancel">
<template #title>设置</template>
<a-space class="mt-3">
<span>表格大小</span>
<a-radio-group type="button" v-model="options.size">
<a-radio value="mini">迷你</a-radio>
<a-radio value="small"></a-radio>
<a-radio value="medium"></a-radio>
<a-radio value="large"></a-radio>
</a-radio-group>
<span class="ml-3">表格边框</span>
<a-radio-group type="button" v-model="bordered" @change="changeBordered">
<a-radio value="hide">不显示外边框</a-radio>
<a-radio value="show">全部显示</a-radio>
<a-radio value="row">不显示行</a-radio>
<a-radio value="column">不显示列</a-radio>
</a-radio-group>
<a-checkbox v-model="options.stripe" class="ml-3">斑马纹</a-checkbox>
</a-space>
<a-alert type="warning" class="mt-2"
>排序本页是指当前页排序服务器是指后台排序若自定义服务器排序可用
<a-tag>@sorterChange</a-tag> 事件来实现</a-alert
>
<a-table
:data="allowShowColumns"
:pagination="false"
:bordered="{ wrapper: true, cell: false }"
:draggable="{ type: 'handle', width: 40 }"
@change="onTableChange"
stripe
class="mt-3"
>
<template #columns>
<a-table-column title="列名称" data-index="title" align="center">
<template #cell="{ record }">{{ record.title }}</template>
</a-table-column>
<a-table-column title="宽度" data-index="width" align="center">
<template #cell="{ record }">
<a-input-number
v-if="!['__index', '__operation'].includes(record.dataIndex)"
style="width: 150px"
placeholder="列宽度"
v-model="record.width"
mode="button"
@change="changeColumn($event, 'width', record.dataIndex)"
/>
<span v-else> / </span>
</template>
</a-table-column>
<a-table-column title="隐藏" data-index="hide" align="center">
<template #cell="{ record }"
><a-checkbox v-model="record.hide" @change="changeColumn($event, 'hide', record.dataIndex)"
/></template>
</a-table-column>
<a-table-column title="固定" data-index="fixed" align="center">
<template #cell="{ record }">
<a-space v-if="!['__index', '__operation'].includes(record.dataIndex)">
<a-radio
v-model="record.fixed"
value=""
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.fixed"
value="left"
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.fixed"
value="right"
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
</a-space>
<span v-else> / </span>
</template>
</a-table-column>
<a-table-column title="排序" data-index="order" align="center">
<template #cell="{ record }">
<a-space v-if="!['__index', '__operation'].includes(record.dataIndex)">
<a-radio
v-model="record.__order"
value=""
@change="changeColumn($event, 'order', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.__order"
value="page"
@change="changeColumn($event, 'order', record.dataIndex)"
>本页</a-radio
>
<a-radio
v-model="record.__order"
value="serve"
@change="changeColumn($event, 'order', record.dataIndex)"
>服务器</a-radio
>
</a-space>
<span v-else> / </span>
</template>
</a-table-column>
</template>
</a-table>
</a-drawer>
</template>
<script setup>
import { ref, inject } from "vue"
const options = inject("options")
let columns = inject("columns")
const allowShowColumns = columns.filter((item) => {
return !(item?.settingHide ?? false)
})
const visible = ref(false)
const bordered = ref("column")
const open = () => {
visible.value = true
}
const onCancel = () => {
visible.value = false
}
const changeColumn = (ev, type, name) => {
const column = columns.find((item) => item.dataIndex === name)
switch (type) {
case "order":
if (ev === "page") {
column.sortable = { sortDirections: ["ascend", "descend"], sorter: false }
} else if (ev === "serve") {
column.sortable = { sortDirections: ["ascend", "descend"], sorter: true }
} else {
column.sortable = undefined
}
break
}
}
const changeBordered = (v) => {
if (v === "hide") {
options.bordered = { wrapper: false, cell: false }
}
if (v === "show") {
options.bordered = { wrapper: true, cell: true }
}
if (v === "row") {
options.bordered = { wrapper: false, cell: true }
}
if (v === "column") {
options.bordered = { wrapper: true, cell: false }
}
}
const onTableChange = (_data) => {
columns = _data
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,876 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-content class="flex flex-col lg:h-full relative w-full">
<div class="_crud-header flex flex-col mb-2" ref="crudHeaderRef">
<a-tabs
v-if="isArray(options.tabs.data) && options.tabs.data.length > 0"
v-model:active-key="options.tabs.defaultKey"
:trigger="options.tabs.trigger"
:type="options.tabs.type"
:hide-content="true"
@change="tabChange"
@tab-click="maEvent.customeEvent(options.tabs, $event, 'onClick')"
class="ma-tabs mb-5"
>
<template #extra><slot name="tabExtra"></slot></template>
<a-tab-pane :key="item.value" :title="item.label" v-for="item in options.tabs.data"></a-tab-pane>
</a-tabs>
<ma-search @search="searchSubmitHandler" class="__search-panel" ref="crudSearchRef">
<template v-for="(slot, slotIndex) in searchSlots" :key="slotIndex" #[slot]="{ searchForm, component }">
<slot :name="`search-${slot}`" v-bind="{ searchForm, component }" />
</template>
<template #searchBeforeButtons>
<slot name="searchBeforeButtons"></slot>
</template>
<template #searchButtons>
<slot name="searchButtons"></slot>
</template>
<template #searchAfterButtons>
<slot name="searchAfterButtons"></slot>
</template>
</ma-search>
</div>
<div class="_crud-content">
<div class="operation-tools lg:flex justify-between mb-3" ref="crudOperationRef">
<a-space class="lg:flex block lg:inline-block">
<slot name="tableBeforeButtons"></slot>
<slot name="tableButtons">
<a-button
v-if="options.add.show"
v-auth="options.add.auth || []"
v-role="options.add.role || []"
@click="addAction"
type="primary"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-plus /></template>{{ options.add.text || "新增" }}
</a-button>
<a-popconfirm content="确定要删除数据吗?" position="bottom" @ok="deletesMultipleAction">
<a-button
v-if="options.delete.show"
v-auth="options.delete.auth || []"
v-role="options.delete.role || []"
type="primary"
status="danger"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-delete /></template>
{{ isRecovery ? options.delete.realText || "删除" : options.delete.text || "删除" }}
</a-button>
</a-popconfirm>
<a-popconfirm content="确定要恢复数据吗?" position="bottom" @ok="recoverysMultipleAction">
<a-button
v-if="options.recovery.show && isRecovery"
v-auth="options.recovery.auth || []"
v-role="options.recovery.role || []"
type="primary"
status="success"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-undo /></template>{{ options.recovery.text || "恢复" }}</a-button
>
</a-popconfirm>
<a-button
v-if="options.import.show"
v-auth="options.import.auth || []"
v-role="options.import.role || []"
@click="importAction"
class="w-full lg:w-auto mt-2 lg:mt-0"
><template #icon><icon-upload /></template>{{ options.import.text || "导入" }}</a-button
>
<a-button
v-if="options.export.show"
v-auth="options.export.auth || []"
v-role="options.export.role || []"
@click="exportAction"
class="w-full lg:w-auto mt-2 lg:mt-0"
><template #icon><icon-download /></template>{{ options.export.text || "导出" }}</a-button
>
<a-button
type="secondary"
@click="handlerExpand"
v-if="options.isExpand"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon>
<icon-expand v-if="!expandState" />
<icon-shrink v-else />
</template>
{{ expandState ? " 折叠" : " 展开" }}
</a-button>
</slot>
<slot name="tableAfterButtons"></slot>
</a-space>
<a-space class="lg:mt-0 mt-2" v-if="options.showTools">
<slot name="tools"></slot>
<a-tooltip
:content="isRecovery ? '显示正常数据' : '显示回收站数据'"
v-if="options.recycleApi && isFunction(options.recycleApi)"
>
<a-button shape="circle" @click="switchDataType"><icon-swap /></a-button>
</a-tooltip>
<a-tooltip content="刷新表格"
><a-button shape="circle" @click="refresh"><icon-refresh /></a-button
></a-tooltip>
<a-tooltip content="显隐搜索"
><a-button shape="circle" @click="toggleSearch"><icon-search /></a-button
></a-tooltip>
<a-tooltip content="打印表格"
><a-button shape="circle" @click="printTable"><icon-printer /></a-button
></a-tooltip>
<a-tooltip content="设置"
><a-button shape="circle" @click="tableSetting"><icon-settings /></a-button
></a-tooltip>
</a-space>
</div>
<div ref="crudContentRef">
<slot name="content" v-bind="tableData">
<a-table
v-bind="$attrs"
ref="tableRef"
:key="options.pk"
:data="tableData"
:loading="loading"
:sticky-header="options.stickyHeader"
:pagination="options.tablePagination"
:stripe="options.stripe"
:bordered="options.bordered"
:rowSelection="options.rowSelection || undefined"
:row-key="options?.rowSelection?.key ?? options.pk"
:scroll="options.scroll"
:column-resizable="options.resizable"
:size="options.size"
:row-class="options.rowClass"
:hide-expand-button-on-empty="options.hideExpandButtonOnEmpty"
:default-expand-all-rows="options.expandAllRows"
:summary="options.customerSummary || __summary || options.showSummary"
@selection-change="setSelecteds"
@sorter-change="handlerSort"
>
<template #tr="{ record }">
<tr
class="ma-crud-table-tr"
:class="
isFunction(options.rowCustomClass)
? options.rowCustomClass(record, rowIndex) ?? []
: options.rowCustomClass
"
@contextmenu.prevent="openContextMenu($event, record)"
@dblclick="dbClickOpenEdit(record)"
/>
</template>
<template #expand-row="record" v-if="options.showExpandRow">
<slot name="expand-row" v-bind="record"></slot>
</template>
<template #columns>
<ma-column
ref="crudColumnRef"
v-if="reloadColumn"
:columns="props.columns"
:isRecovery="isRecovery"
:crudFormRef="crudFormRef"
@refresh="() => refresh()"
@showImage="showImage"
>
<template #operationBeforeExtend="{ record, column, rowIndex }">
<slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template #operationCell="{ record, column, rowIndex }">
<slot name="operationCell" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template #operationAfterExtend="{ record, column, rowIndex }">
<slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template
v-for="(slot, slotIndex) in slots"
:key="slotIndex"
#[slot]="{ record, column, rowIndex }"
>
<slot :name="`${slot}`" v-bind="{ record, column, rowIndex }" />
</template>
</ma-column>
</template>
<template
#summary-cell="{ column, record, rowIndex }"
v-if="options.customerSummary || options.showSummary"
>
<slot name="summaryCell" v-bind="{ record, column, rowIndex }">{{
record[column.dataIndex]
}}</slot>
</template>
</a-table>
</slot>
</div>
</div>
<div
class="_crud-footer mt-3 text-right"
ref="crudFooterRef"
v-if="total > 0 && openPagination && !options.tablePagination"
>
<a-pagination
:total="total"
show-total
show-jumper
show-page-size
:page-size-options="options.pageSizeOption"
@page-size-change="pageSizeChangeHandler"
@change="pageChangeHandler"
v-model:current="requestParams[config.request.page]"
:page-size="requestParams[config.request.pageSize]"
style="display: inline-flex"
/>
</div>
<ma-setting ref="crudSettingRef" />
<ma-form ref="crudFormRef" @success="requestSuccess">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-form>
<ma-import ref="crudImportRef" />
<ma-context-menu ref="crudContextMenuRef" @execCommand="execContextMenuCommand" />
<a-image-preview :src="imgUrl" v-model:visible="imgVisible" />
</a-layout-content>
</template>
<script setup>
import config from "@/config/crud"
import { ref, watch, provide, nextTick, onMounted, onUnmounted } from "vue"
import defaultOptions from "./js/defaultOptions"
import { loadDict } from "@cps/ma-form/js/networkRequest.js"
import ColumnService from "./js/columnService"
import MaSearch from "./components/search.vue"
import MaForm from "./components/form.vue"
import MaSetting from "./components/setting.vue"
import MaImport from "./components/import.vue"
import MaColumn from "./components/column.vue"
import MaContextMenu from "./components/contextMenu.vue"
import checkAuth from "@/directives/auth/auth"
import checkRole from "@/directives/role/role"
import { Message } from "@arco-design/web-vue"
import { request } from "@/utils/request"
import tool from "@/utils/tool"
import Print from "@/utils/print"
import { isArray, isFunction, isObject, isUndefined } from "lodash"
import { maEvent } from "@cps/ma-form/js/formItemMixin.js"
import globalColumn from "@/config/column.js"
import { useFormStore } from "@/store/index"
const formStore = useFormStore()
const props = defineProps({
// 表格数据
data: { type: [Function, Array], default: () => null },
// 增删改查设置
options: { type: Object, default: {} },
crud: { type: Object, default: {} },
// 字段列设置
columns: { type: Array, default: [] }
})
const loading = ref(true)
const dicts = ref({})
const cascaders = ref([])
const reloadColumn = ref(true)
const openPagination = ref(false)
const imgVisible = ref(false)
const imgUrl = ref(import.meta.env.VITE_APP_BASE + "not-image.png")
const total = ref(0)
const requestParams = ref({})
const slots = ref([])
const searchSlots = ref([])
const isRecovery = ref(false)
const expandState = ref(false)
const crudHeaderRef = ref()
const crudOperationRef = ref()
const crudContentRef = ref()
const crudFooterRef = ref()
const crudSearchRef = ref()
const crudSettingRef = ref()
const crudFormRef = ref()
const crudImportRef = ref()
const crudColumnRef = ref()
const crudContextMenuRef = ref()
const options = ref(Object.assign(JSON.parse(JSON.stringify(defaultOptions)), props.options, props.crud))
const columns = ref(props.columns)
const headerHeight = ref(0)
const selecteds = ref([])
const tableData = ref([])
const tableRef = ref()
const currentApi = ref()
// 初始化
const init = async () => {
// 设置 组件id
if (isUndefined(options.value.id)) {
options.value.id = "MaCrud_" + Math.floor(Math.random() * 100000 + Math.random() * 20000 + Math.random() * 5000)
}
// 收集数据
props.columns.map((item) => {
if (item.cascaderItem && item.cascaderItem.length > 0) {
cascaders.value.push(...item.cascaderItem)
}
})
await props.columns.map(async (item) => {
// 字典
if (!cascaders.value.includes(item.dataIndex) && item.dict) {
await loadDict(dicts.value, item)
}
})
await tabsHandler()
}
const dictTrans = (dataIndex, value) => {
if (dicts.value[dataIndex] && dicts.value[dataIndex].tran) {
return dicts.value[dataIndex].tran[value]
} else {
return value
}
}
const dictColors = (dataIndex, value) => {
if (dicts.value[dataIndex] && dicts.value[dataIndex].colors) {
return dicts.value[dataIndex].colors[value]
} else {
return undefined
}
}
// 公用模板
columns.value.map((item, index) => {
if (item.common && globalColumn[item.dataIndex]) {
columns.value[index] = globalColumn[item.dataIndex]
item = columns.value[index]
}
!item.width && (item.width = options.value.columnWidth)
})
provide("options", options.value)
provide("columns", props.columns)
provide("layout", props.layout)
provide("dicts", dicts.value)
provide("dictColors", dictColors.value)
provide("requestParams", requestParams.value)
provide("dictTrans", dictTrans)
provide("dictColors", dictColors)
provide("isRecovery", isRecovery)
watch(
() => props.options.api,
(vl) => (options.value.api = vl)
)
watch(
() => props.crud.api,
(vl) => (options.value.api = vl)
)
watch(
() => openPagination.value,
() => options.value.pageLayout === "fixed" && settingFixedPage()
)
watch(
() => formStore.crudList[options.value.id],
async (vl) => {
vl === true && (await requestData())
formStore.crudList[options.value.id] = false
}
)
const getSlot = (cls = []) => {
let sls = []
cls.map((item) => {
if (item.children && item.children.length > 0) {
let tmp = getSlot(item.children)
sls.push(...tmp)
} else if (item.dataIndex) {
sls.push(item.dataIndex)
}
})
return sls
}
const showImage = (url) => {
imgUrl.value = url
imgVisible.value = true
}
const getSearchSlot = () => {
let sls = []
props.columns.map((item) => {
if (item.search && item.search === true) {
sls.push(item.dataIndex)
}
})
return sls
}
slots.value = getSlot(props.columns)
searchSlots.value = getSearchSlot(props.columns)
const requestData = async () => {
await init()
if (options.value.showIndex && columns.value.length > 0 && columns.value[0].dataIndex !== "__index") {
columns.value.unshift({
title: options.value.indexLabel,
dataIndex: "__index",
width: options.value.indexColumnWidth,
fixed: options.value.indexColumnFixed
})
}
if (
options.value.operationColumn &&
columns.value.length > 0 &&
columns.value[columns.value.length - 1].dataIndex !== "__operation"
) {
columns.value.push({
title: options.value.operationColumnText,
dataIndex: "__operation",
width: options.value.operationColumnWidth ?? options.value.operationWidth,
align: options.value.operationColumnAlign,
fixed: options.value.operationColumnFixed
})
}
initRequestParams()
if (!options.value.tabs?.dataIndex && !options.value.tabs.data) {
await refresh()
} else {
options.value.tabs.defaultKey = options.value.tabs?.defaultKey ?? options.value.tabs.data[0].value
await tabChange(options.value.tabs?.defaultKey)
}
}
const initRequestParams = () => {
requestParams.value[config.request.page] = 1
requestParams.value[config.request.pageSize] = options.value.pageSize ?? 10
if (options.value.requestParamsLabel) {
requestParams.value[options.value.requestParamsLabel] = options.value.requestParams
} else {
requestParams.value = Object.assign(requestParams.value, options.value.requestParams)
}
}
const requestHandle = async () => {
loading.value = true
isFunction(options.value.beforeRequest) && options.value.beforeRequest(requestParams.value)
if (isFunction(currentApi.value)) {
const response = config.parseResponseData(await currentApi.value(requestParams.value))
if (response.rows) {
tableData.value = response.rows
if (response.pageInfo) {
total.value = response.pageInfo.total
openPagination.value = true
} else {
openPagination.value = false
}
} else {
tableData.value = response
}
} else {
console.error(`ma-crud errorcrud.api not is Function.`)
}
isFunction(options.value.afterRequest) && options.value.afterRequest(tableData.value)
loading.value = false
}
const refresh = async () => {
if (props.data) {
loading.value = true
const data = isArray(props.data) ? props.data : config.parseResponseData(await props.data(requestParams.value))
if (data.rows) {
tableData.value = data.rows
openPagination.value = true
} else {
tableData.value = data
}
loading.value = false
} else {
currentApi.value =
isRecovery.value && options.value.recycleApi && isFunction(options.value.recycleApi)
? options.value.recycleApi
: options.value.api
await requestHandle()
}
}
const searchSubmitHandler = async (formData) => {
if (options.value.requestParamsLabel && requestParams.value[options.value.requestParamsLabel]) {
requestParams.value[options.value.requestParamsLabel] = Object.assign(
requestParams.value[options.value.requestParamsLabel],
formData
)
} else if (options.value.requestParamsLabel) {
requestParams.value[options.value.requestParamsLabel] = Object.assign({}, formData)
} else {
requestParams.value = Object.assign(requestParams.value, formData)
}
if (options.value.beforeSearch && isFunction(options.value.beforeSearch)) {
options.value.beforeSearch(requestParams.value)
}
await pageChangeHandler(1)
if (options.value.afterSearch && isFunction(options.value.afterSearch)) {
options.value.afterSearch(requestParams.value)
}
}
const pageSizeChangeHandler = async (pageSize) => {
requestParams.value[config.request.page] = 1
requestParams.value[config.request.pageSize] = pageSize
await refresh()
}
const pageChangeHandler = async (currentPage) => {
requestParams.value[config.request.page] = currentPage
await refresh()
}
const toggleSearch = async () => {
const dom = crudHeaderRef.value?.style
if (dom) {
crudSearchRef.value.showSearch ? crudSearchRef.value.setSearchHidden() : crudSearchRef.value.setSearchDisplay()
await nextTick(() => {
headerHeight.value = crudHeaderRef.value.offsetHeight
options.value.pageLayout === "fixed" && settingFixedPage()
})
}
}
const settingFixedPage = () => {
const workAreaHeight = document.querySelector(".work-area").offsetHeight
const tableHeight = workAreaHeight - headerHeight.value - (openPagination.value ? 152 : 108)
crudContentRef.value.style.height = tableHeight + "px"
}
const tableSetting = () => {
crudSettingRef.value.open()
}
const requestSuccess = async (response) => {
if (response && response.code && response.code === 200) {
options.value.dataCompleteRefresh && (await refresh())
if (reloadColumn.value) {
reloadColumn.value = false
await nextTick(() => {
reloadColumn.value = true
})
}
}
}
const addAction = () => {
if (isFunction(options.value.beforeOpenAdd) && !options.value.beforeOpenAdd()) {
return false
}
if (options.value.add.action && isFunction(options.value.add.action)) {
options.value.add.action()
} else {
crudFormRef.value.add()
}
}
const editAction = (record) => {
if (isFunction(options.value.beforeOpenEdit) && !options.value.beforeOpenEdit(record)) {
return false
}
if (options.value.edit.action && isFunction(options.value.edit.action)) {
options.value.edit.action(record)
} else {
crudFormRef.value.edit(record)
}
}
const dbClickOpenEdit = (record) => {
if (options.value.isDbClickEdit) {
if (isRecovery.value) {
Message.error("回收站数据不可编辑")
return
}
if (isArray(options.value.edit.auth)) {
for (let index in options.value.edit.auth) {
if (!checkAuth(options.value.edit.auth[index])) {
Message.error("没有编辑数据的权限")
return
}
}
if (options.value.edit.api && isFunction(options.value.edit.api)) {
editAction(record)
}
}
}
}
const importAction = () => crudImportRef.value.open()
const exportAction = () => {
Message.info("请求服务器下载文件中...")
const data = options.value.requestParamsLabel
? requestParams.value[options.value.requestParamsLabel]
: requestParams.value
const download = (url) => request({ url, data, method: "post", timeout: 60 * 1000, responseType: "blob" })
download(options.value.export.url)
.then((res) => {
tool.download(res)
Message.success("请求成功,文件开始下载")
})
.catch(() => {
Message.error("请求服务器错误,下载失败")
})
}
const deletesMultipleAction = async () => {
if (selecteds.value && selecteds.value.length > 0) {
const api = isRecovery.value ? options.value.delete.realApi : options.value.delete.api
let data = {}
if (isFunction(options.value.beforeDelete) && !(data = options.value.beforeDelete(selecteds.value))) {
return false
}
const response = await api(Object.assign({ ids: selecteds.value }, data))
if (options.value.afterDelete && isFunction(options.value.afterDelete)) {
options.value.afterDelete(response)
}
response.success && Message.success(response.message || `删除成功!`)
await refresh()
} else {
Message.error("至少选择一条数据")
}
}
const recoverysMultipleAction = async () => {
if (selecteds.value && selecteds.value.length > 0) {
const response = await options.value.recovery.api({ ids: selecteds.value })
response.success && Message.success(response.message || `恢复成功!`)
await refresh()
} else {
Message.error("至少选择一条数据")
}
}
const setSelecteds = (key) => {
selecteds.value = key
}
const switchDataType = async () => {
isRecovery.value = !isRecovery.value
currentApi.value =
isRecovery.value && options.value.recycleApi && isFunction(options.value.recycleApi)
? options.value.recycleApi
: options.value.api
await requestData()
}
const handlerExpand = () => {
expandState.value = !expandState.value
expandState.value ? tableRef.value.expandAll(true) : tableRef.value.expandAll(false)
}
const handlerSort = async (name, type) => {
const col = columns.value.find((item) => name === item.dataIndex)
if (col.sortable && col.sortable.sorter) {
if (type) {
requestParams.value.orderBy = name
requestParams.value.orderType = type === "ascend" ? "asc" : "desc"
} else {
requestParams.value.orderBy = undefined
requestParams.value.orderType = undefined
}
await refresh()
}
}
const getTableData = () => {
return tableData.value
}
const __summary = ({ data }) => {
if (options.value.showSummary && isArray(options.value.summary)) {
const summary = options.value.summary
let summaryData = {}
let summaryPrefixText = {}
let summarySuffixText = {}
let length = data.length || 0
summary.map((item) => {
summaryData[item.dataIndex] = 0
summaryPrefixText[item.dataIndex] = item?.prefixText ?? ""
summarySuffixText[item.dataIndex] = item?.suffixText ?? ""
data.map((record) => {
if (record[item.dataIndex]) {
if (item.action && item.action === "sum") {
summaryData[item.dataIndex] += parseFloat(record[item.dataIndex])
}
if (item.action && item.action === "avg") {
summaryData[item.dataIndex] += parseFloat(record[item.dataIndex]) / length
}
}
})
})
for (let i in summaryData) {
summaryData[i] =
summaryPrefixText[i] + tool.groupSeparator(summaryData[i].toFixed(2)) + summarySuffixText[i]
}
return [summaryData]
}
}
const resizeHandler = () => {
headerHeight.value = crudHeaderRef.value.offsetHeight
settingFixedPage()
}
const tabChange = async (value) => {
const searchKey = options.value.tabs?.searchKey ?? options.value.tabs?.dataIndex ?? "tabValue"
const params = {}
params[searchKey] = value
requestParams.value = Object.assign(requestParams.value, params)
await maEvent.customeEvent(options.value.tabs, value, "onChange")
await refresh()
}
const printTable = () => {
new Print(crudContentRef.value)
}
const openContextMenu = (ev, record) => {
options.value?.contextMenu?.enabled === true && crudContextMenuRef.value.openContextMenu(ev, record)
}
const execContextMenuCommand = async (args) => {
const item = args.contextItem
const record = args.record
switch (item.operation) {
case "print":
await printTable()
break
case "refresh":
await refresh()
break
case "add":
addAction()
break
case "edit":
editAction(record)
break
case "delete":
crudColumnRef.value.deleteAction(record)
break
default:
await maEvent.customeEvent(item, args, "onCommand")
break
}
}
const tabsHandler = async () => {
// 处理tabs
const tabs = options.value.tabs
if (isFunction(tabs.data) || isArray(tabs.data)) {
tabs.data = isFunction(tabs.data) ? await tabs.data() : tabs.data
} else if (!isUndefined(tabs.dataIndex)) {
const col = props.columns.find((item) => item.dataIndex === tabs.dataIndex)
if (col.search === true && isObject(col.dict)) {
tabs.data = dicts.value[tabs.dataIndex]
}
}
}
onMounted(async () => {
if (typeof options.value.autoRequest == "undefined" || options.value.autoRequest) {
await requestData()
}
if (!options.value.expandSearch && crudSearchRef.value) {
crudSearchRef.value.setSearchHidden()
}
if (options.value.pageLayout === "fixed") {
window.addEventListener("resize", resizeHandler, false)
headerHeight.value = crudHeaderRef.value.offsetHeight
settingFixedPage()
}
})
onUnmounted(() => {
if (options.value.pageLayout === "fixed") {
window.removeEventListener("resize", resizeHandler, false)
}
})
const getCurrentAction = () => crudFormRef.value.currentAction
const getFormData = () => crudFormRef.value.form
const getFormColumns = async (type = "add") => {
return await crudFormRef.value.getFormColumns(type)
}
/**
* 获取column属性服务类
* @returns ColumnService
*/
const getColumnService = (strictMode = true) => {
return new ColumnService({ columns: columns.value, cascaders: cascaders.value, dicts: dicts.value }, strictMode)
}
defineExpose({
refresh,
requestData,
addAction,
editAction,
getTableData,
setSelecteds,
getCurrentAction,
getFormData,
getFormColumns,
getColumnService,
requestParams,
isRecovery,
tableRef,
crudFormRef,
crudSearchRef,
crudImportRef,
crudSettingRef
})
</script>
<style scoped lang="less">
.__search-panel {
transition: display 1s;
overflow: hidden;
width: 100%;
}
._crud-footer {
z-index: 10;
}
</style>

View File

@@ -0,0 +1,111 @@
import { loadDict } from "@cps/ma-form/js/networkRequest"
/**
* columnService 列服务处理类
* 首先感谢 @NEKGod 提交的PR此功能原本写在了 Ma-Crud 组件,我特意摘出来,封装成类通过引用来调用
* @author NEKGod, X.Mo <root@imoi.cn>
*/
const objectService = function (item) {
this.setAttr = (key, value) => {
item[key] = value
}
this.getAttr = (key) => {
return item[key]
}
this.get = () => {
return item
}
this.set = (config = {}) => {
for (let [key, value] of Object.entries(config)) {
item[key] = value
}
}
}
class ColumnService {
/**
* @type {Map<string, Object>}
*/
columnMap = new Map()
columns
cascaders
dicts
strictMode
/**
* @param data
* @param strictMode
*/
constructor(data, strictMode) {
this.columns = data.columns
this.cascaders = data.cascaders
this.dicts = data.dicts
this.strictMode = strictMode
this.columns.forEach((item) => {
this.columnMap.set(item.dataIndex, new objectService(item))
})
}
get(dataIndex) {
return this.columnMap.get(dataIndex)
}
isEmpty(dataIndex) {
return !this.columnMap.has(dataIndex)
}
exist(dataIndex) {
return !this.isEmpty(dataIndex)
}
async append(item, appendStartDataIndex = null) {
if (this.strictMode === true && item.dataIndex && this.exist(item.dataIndex)) {
console.warn(
`严格模式columnService.append(item) 参数中未有item.dataIndex属性或者item.dataIndex已存在column.${item.dataIndex}`
)
return false
}
if (this.cascaders.includes(item.dataIndex) && item.dict) {
await loadDict(this.dicts, item)
}
this.columns.push(item)
this.columnMap.set(item.dataIndex, new objectService(item))
// 获取排序
if (appendStartDataIndex !== null) {
let appendIndex =
this.columns
.map((item) => {
return item.dataIndex
})
?.indexOf(appendStartDataIndex) ?? -1
if (appendIndex === -1) {
return this.append(item, null)
}
let sortIndex = 0
let appendPosIndex = 0
this.columns.forEach((sortItem) => {
if (sortItem.dataIndex === appendStartDataIndex) {
appendPosIndex = sortIndex
} else if (sortItem.dataIndex === item.dataIndex) {
sortIndex = appendPosIndex + 1
} else {
}
sortItem.sortIndex = sortIndex
sortIndex += 2
})
this.columns.sort((a, b) => a.sortIndex - b.sortIndex)
}
return true
}
}
export default ColumnService

View File

@@ -0,0 +1,16 @@
import { defineComponent } from "vue"
export default defineComponent({
name: "CustomRender",
props: {
record: Object,
render: Function,
rowIndex: Number,
column: {
type: Object,
default: null
}
},
render() {
return this.render({ record: this.record, column: this.column, rowIndex: this.rowIndex })
}
})

View File

@@ -0,0 +1,277 @@
export default {
// 当前crud组件的 id全局唯一不指定则随机生成一个
id: undefined,
// 主键名称
pk: "id",
// 表单是否排除PK
formExcludePk: true,
// 请求api方法
api: () => {},
// 请求回收站api方法
recycleApi: () => {},
// 是否自动请求
autoRequest: true,
// 请求参数
requestParams: {},
// 设置分页组件每页记录数
pageSizeOption: [10, 20, 30, 50, 100],
// 是否开启表格分页
tablePagination: false,
// 设置选择列
rowSelection: undefined,
// 是否显示边框
bordered: { wrapper: true, cell: false },
// 是否开启拖拽排序
dragSort: false,
// 每页记录数
pageSize: 10,
// 子节点为空隐藏节点按钮
hideExpandButtonOnEmpty: true,
// 默认展开所有行
expandAllRows: false,
// 默认展开搜索
expandSearch: true,
// 斑马线
stripe: true,
// 新增、编辑、删除完成后是否刷新表格
dataCompleteRefresh: true,
// 表格大小
size: "large",
// 是否开启双击编辑数据
isDbClickEdit: true,
// 是否显示展开/折叠按钮
isExpand: false,
// 是否显示自定义
showExpandRow: false,
// 是否显示总结行
showSummary: false,
// 自定义总结行,要传入函数
customerSummary: false,
// 是否显示工具栏
showTools: true,
// 表头是否吸顶
stickyHeader: true,
// 页面布局方式,支持 normal标准和 fixed固定两种
pageLayout: "normal",
// 默认统一设置列宽度
columnWidth: 100,
// 搜索标签对齐方式
searchLabelAlign: "right",
// 全局搜索标签宽度
searchLabelWidth: "85px",
// 搜索每行列数
searchColNumber: 4,
// 搜索提交按钮文案
searchSubmitButtonText: "搜索",
// 搜索重置按钮文案
searchResetButtonText: "重置",
// 搜索栏加载提示文案
searchLoadingText: "加载数据中...",
// 搜索提交前置方法
beforeSearch: (requestParams) => {},
// 搜索提交后置方法
afterSearch: (requestParams) => {},
// 重置搜索钩子
resetSearch: (searchData) => {},
// 请求前置处理
beforeRequest: (requestParams) => {},
// 请求后置处理
afterRequest: (tableData) => {},
// 新增打开前方法
beforeOpenAdd: () => {},
// 新增提交前方法
beforeAdd: (formData) => {},
// 新增提交后方法
afterAdd: (response, formData) => {},
// 编辑打开前方法
beforeOpenEdit: (record) => {},
// 编辑提交前方法
beforeEdit: (formData) => {},
// 编辑提交后方法
afterEdit: (response, formData) => {},
// 删除前方法
beforeDelete: (ids) => {},
// 删除后方法
afterDelete: (response) => {},
// 组件初始化事件
onInit: () => {},
// 列表 选项卡 参数配置项
tabs: {
// 选项卡类型,参考 arco 官方 tabs 的api
type: "line",
// 选项卡触发方式: click | hover
trigger: "click",
// 指定一个字段作为选项卡,该字段的 search 必须为 true 并且使用了字典
dataIndex: undefined,
// 自定义选项卡项 [{ label: 'tab 1', value: 1, disabled: false }],也可函数返回一个数组
data: undefined,
// 默认选中的 tab
defaultKey: undefined,
// 切换选项卡时,请求后台数据的参数名
searchKey: undefined,
// 选项卡切换事件
onChange: (value) => {},
// 选项卡单击事件
onClick: (value) => {}
},
// 表单配置项
formOption: {
// 显示方式支持模态框和抽屉: modal drawer tag
viewType: "modal",
// 只有 viewType 为 tag 时生效,此值在所有 MaCrud 内唯一
tagId: "",
// 只有 viewType 为 tag 时生效tag标题名称
tagName: "",
// tag页设置标签标题的字段名称
titleDataIndex: undefined,
// 显示宽度
width: 600,
// 是否全屏只有modal有效
isFull: false,
// 表单布局
layout: []
},
add: {
// 新增api
api: undefined,
// 显示新增按钮的权限
auth: [],
// 显示新增按钮的角色
role: [],
// 按钮文案
text: "新增",
// 是否显示
show: false
},
edit: {
// 编辑api
api: undefined,
// 显示编辑按钮的权限
auth: [],
// 显示编辑按钮的角色
role: [],
// 按钮文案
text: "编辑",
// 是否显示
show: false
},
delete: {
// 删除api
api: undefined,
// 显示删除按钮的权限
auth: [],
// 显示删除按钮的角色
role: [],
// 按钮文案
text: "删除",
// 真实删除api
realApi: undefined,
// 显示真实删除按钮的权限
realAuth: [],
// 显示真实删除按钮的角色
realRole: [],
// 真实按钮文案
realText: "删除",
// 是否显示
show: false
},
recovery: {
// 恢复api
api: undefined,
// 显示恢复按钮的权限
auth: [],
// 显示恢复按钮的角色
role: [],
// 按钮文案
text: "恢复",
// 是否显示
show: false
},
// see: {
// // 显示查看按钮的权限
// auth: [],
// // 显示查看按钮的角色
// role: [],
// // 按钮文案
// text: '查看',
// // 是否显示
// show: false,
// },
import: {
// 导入url
url: undefined,
// 下载模板地址
templateUrl: undefined,
// 显示导入按钮的权限
auth: [],
// 显示导入按钮的角色
role: [],
// 按钮文案
text: "导入",
// 是否显示
show: false
},
export: {
// 导出url
url: undefined,
// 显示导出按钮的权限
auth: [],
// 显示导出按钮的角色
role: [],
// 按钮文案
text: "导出",
// 是否显示
show: false
},
// 行自定义 class 名称
rowCustomClass: (record, rowIndex) => [],
// 是否显示索引列
showIndex: false,
// 索引列名称
indexLabel: "序号",
// 索引列宽度
indexColumnWidth: 70,
// 索引列固定方向false 为不固定
indexColumnFixed: "left",
// 设置请求数据label
requestParamsLabel: undefined,
// 表格滚动默认宽高
scroll: { x: "100%", y: "100%" },
// 调整列宽
resizable: true,
// 是否显示操作列
operationColumn: false,
// 操作列宽度
operationWidth: 160,
// 操作列宽度 (新api)
operationColumnWidth: 160,
// 操作列名称
operationColumnText: "操作",
// 操作列文字对齐方式
operationColumnAlign: "right",
// 操作列固定方向false 为不固定
operationColumnFixed: "right",
// 右键菜单配置
contextMenu: {
// 是否开启右键菜单
enabled: true,
// 右键菜单配置
items: [
{ operation: "print" },
{ operation: "refresh" },
{ operation: "divider" },
{ operation: "add" },
{ operation: "edit" },
{ operation: "delete" }
]
}
}

View File

@@ -0,0 +1,116 @@
import { VNodeChild } from "vue"
/**
* 表单组件类型
*/
import { FieldRule } from "@arco-design/web-vue"
export type FormDateType =
| "radio"
| "checkbox"
| "select"
| "transfer"
| "tree-select"
| "cascader"
| "date"
| "month"
| "year"
| "week"
| "quarter"
| "range"
| "time"
| "input"
| "password"
| "textarea"
| "upload"
| "select-user"
| "editor"
| "code-editor"
| "icon"
| "user-info"
| "city-linkage"
| "form-group"
| "select-resource"
/**
* 列字典
*/
export interface ColumnDict {
// 字典名称,快捷查询字典接口查询
name?: string
// 自定义url查询
url?: string
// url查询方法,填写url之后生效
method?: "GET" | "POST" | "PUT" | "DELETE"
// url查询params数据,填写url之后生效
params?: object
// url查询body数据,填写url之后生效
body?: object
// 直接设置字典值
data?: object | Function
// 表格列的值是否翻译为字典对应标签
translation?: boolean
// 表格key 和 value的props设置
props?: {
label?: string
value?: string
}
}
export interface BasicColumn {
// 标题
title: string
// 字段名称
dataIndex: string
// 组件类型
formType?: FormDateType
// 表格列对齐方式
align?: "center" | "right" | "left"
// 字段是否加入搜索
search?: boolean
// 列宽
width?: number | "auto"
// 表格列是否隐藏
hide?: boolean
// 编辑|创建 通用是否显示字段
display?: boolean
// 添加弹窗是否显示字段
addDisplay?: boolean
// 编辑弹窗是否显示字段
editDisplay?: boolean
// 编辑|创建 通用是否禁用字段
disabled?: boolean
// 添加弹窗是否禁用字段
addDisabled?: boolean
// 编辑弹窗是否禁用字段
editDisabled?: boolean
// 编辑|创建 通用是否只读字段
readonly?: boolean
// 添加弹窗是否只读字段
addReadonly?: boolean
// 编辑弹窗是否只读字段
editReadonly?: boolean
// 自定义渲染
customRender?: (({ record, column, rowIndex }) => VNodeChild | JSX.Element) | VNodeChild | JSX.Element
// 字段新增时默认值
addDefaultValue?: number | string | boolean | undefined | ((record) => number | string | boolean | undefined)
// 字段编辑时默认值
editDefaultValue?: number | string | boolean | undefined | ((record) => number | string | boolean | undefined)
// select,radio,treeSelect,下拉字典配置
dict?: ColumnDict
// 继承公用配置
common?: boolean
// select 和 tree-select 组件是否开启虚拟列表
virtualList?: boolean
// 搜索默认值
searchDefaultValue?: number | string | undefined
// 搜索描述
searchPlaceholder?: string
//编辑|创建 通用规则
commonRules?: FieldRule | FieldRule[]
// 创建时规则
addRules?: FieldRule | FieldRule[]
// 编辑时规则
editRules?: FieldRule | FieldRule[]
// 子表单数据
children?: BasicColumn[]
}

View File

@@ -0,0 +1,189 @@
export interface BasicCrud {
// 表格接口
api?: undefined | any
// 主键名称
pk?: string
// 设置选择列
rowSelection?:
| undefined
| {
// 选择值的标识默认id
key?: string
// 选择列是否显示全选
showCheckedAll?: boolean
// 行选择器类型
type?: "checkbox" | "radio"
// 选择器列标题
title?: string | "#"
// 列宽度
width?: number | 60
// 是否固定
fixed?: boolean | false
// 是否仅展示当前页的keys
onlyCurrent?: boolean | true
}
// 搜索label宽度
searchLabelWidth?: string | "auto"
// 搜索label对齐方式
searchLabelAlign?: "center" | "right" | "left"
// 一行多少列
searchLabelCols?: number
// 是否显示边框
bordered?: { wrapper?: boolean; cell?: boolean }
// 是否开启拖拽排序
dragSort?: boolean
// 子节点为空隐藏节点按钮
hideExpandButtonOnEmpty?: boolean
// 默认展开所有行
expandAllRows?: boolean
// 斑马线
stripe?: boolean
// 新增、编辑、删除完成后是否刷新表格
dataCompleteRefresh?: boolean
// 表格大小
size?: "mini" | "small" | "medium" | "large"
// 是否开启双击编辑数据
isDbClickEdit?: boolean
// 是否显示展开/折叠按钮
isExpand?: boolean
// 是否显示自定义
showExpandRow?: boolean
// 是否显示总结行
showSummary?: boolean
// 自定义总结行,要传入函数
customerSummary?: boolean
// 是否显示工具栏
showTools?: boolean
// 新增和编辑显示设置
formOption?: {
// 显示方式支持模态框和抽屉?: modal drawer
viewType?: "modal" | "drawer"
// 显示宽度
width?: number
// 是否全屏只有modal有效
isFull?: boolean
}
//新增确定之前修改form值
beforeAdd?: (form) => void
//新增确定之后调用,返回接口response和form值
afterAdd?: (response, form) => void
//编辑确定之前修改form值
beforeEdit?: (form) => void
//编辑确定之后调用,返回接口response和form值
afterEdit?: (response, form) => void
add?: {
// 新增api
api?: undefined | any
// 显示新增按钮的权限
auth?: string[]
// 显示新增按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
edit?: {
// 编辑api
api?: undefined | any
// 显示编辑按钮的权限
auth?: string[]
// 显示编辑按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
delete?: {
// 删除api
api?: undefined | any
// 显示删除按钮的权限
auth?: string[]
// 显示删除按钮的角色
role?: string[]
// 按钮文案
text?: string
// 真实删除api
realApi?: undefined | any
// 显示真实删除按钮的权限
realAuth?: string[]
// 显示真实删除按钮的角色
realRole?: string[]
// 真实按钮文案
realText?: string
// 是否显示
show?: boolean
}
// Todo
recycleApi?: any
recovery?: {
// 显示恢复按钮的权限
auth?: string[]
// 显示恢复按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
// 恢复列表查询api
api?: undefined | any
}
// see?: {
// // 显示查看按钮的权限
// auth?: string[]
// // 显示查看按钮的角色
// role?: string[]
// // 按钮文案
// text?: string
// // 是否显示
// show?: boolean
// }
import?: {
// 导入url
url?: undefined | any
// 下载模板地址
templateUrl?: undefined | any
// 显示导入按钮的权限
auth?: string[]
// 显示导入按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
export?: {
// 导出url
url?: undefined | any
// 显示导出按钮的权限
auth?: string[]
// 显示导出按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
// 是否显示索引列
showIndex?: boolean
// 索引列名称
indexLabel?: string
// 设置请求数据label
requestParamsLabel?: undefined
// 表格滚动默认宽高
scroll?: {
x?: string
y?: string
}
// 调整列宽
resizable?: boolean
// 是否显示操作列
operationColumn?: boolean
// 操作列宽度
operationWidth?: number
// 操作列名称
operationColumnText?: string
}

View File

@@ -0,0 +1,2 @@
export * from "./columns"
export * from "./crud"

View File

@@ -0,0 +1,146 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div>
<editor :key="editorKey" v-model="content" :init="initConfig" :id="props.id"></editor>
<a-modal v-model:visible="resourceVisible" :width="1080" :footer="false" draggable>
<template #title>资源选择器</template>
<ma-resource v-model="list" multiple ref="resource" />
</a-modal>
</div>
</template>
<script setup>
import { reactive, ref, watch, computed } from "vue"
import MaResource from "@cps/ma-resource/index.vue"
import { useAppStore } from "@/store"
import Editor from "@tinymce/tinymce-vue"
import tinymce from "tinymce/tinymce"
import "tinymce/icons/default"
import "tinymce/models/dom"
import "tinymce/themes/silver"
import "tinymce/plugins/advlist" //高级列表
import "tinymce/plugins/anchor" //锚点
import "tinymce/plugins/autolink" //自动链接
import "tinymce/plugins/autosave" //自动存稿
import "tinymce/plugins/charmap" //特殊字符
import "tinymce/plugins/code" //编辑源码
import "tinymce/plugins/codesample" //代码示例
import "tinymce/plugins/directionality" //文字方向
import "tinymce/plugins/emoticons" //表情
import "tinymce/plugins/fullscreen" //全屏
import "tinymce/plugins/help" //帮助
import "tinymce/plugins/image" //插入编辑图片
import "tinymce/plugins/importcss" //引入css
import "tinymce/plugins/insertdatetime" //插入日期时间
import "tinymce/plugins/link" //超链接
import "tinymce/plugins/lists" //列表插件
import "tinymce/plugins/media" //插入编辑媒体
import "tinymce/plugins/nonbreaking" //插入不间断空格
import "tinymce/plugins/pagebreak" //插入分页符
import "tinymce/plugins/preview" //预览
import "tinymce/plugins/quickbars" //快速工具栏
import "tinymce/plugins/save" //保存
import "tinymce/plugins/searchreplace" //查找替换
import "tinymce/plugins/table" //表格
import "tinymce/plugins/template" //内容模板
import "tinymce/plugins/visualblocks" //显示元素范围
import "tinymce/plugins/visualchars" //显示不可见字符
import "tinymce/plugins/wordcount" //字数统计
const appStore = useAppStore()
const props = defineProps({
modelValue: { type: String },
height: { type: Number, default: 400 },
id: { type: String, default: () => "tinymce" + new Date().getTime().toString() },
plugins: {
type: [String, Array],
default:
"preview searchreplace autolink directionality visualblocks visualchars fullscreen link media template code codesample table charmap nonbreaking insertdatetime advlist lists wordcount autosave"
},
toolbar: {
type: [String, Array],
default:
"code undo redo restoredraft | paste pastetext | forecolor backcolor bold italic underline strikethrough link codesample | fullscreen preview | alignleft aligncenter alignright alignjustify outdent indent formatpainter | \
styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat | table media \
charmap pagebreak insertdatetime | resource"
}
})
const emit = defineEmits(["update:modelValue", "change"])
let content = computed({
get() {
return props.modelValue
},
set(value) {
emit("update:modelValue", value)
}
})
const list = ref([])
const resource = ref()
const resourceVisible = ref(false)
const initConfig = reactive({
menubar: false, // 菜单栏显隐
language_url: "/tinymce/i18n/zh_CN.js",
language: "zh_CN",
skin_url: appStore.mode === "light" ? "/tinymce/skins/ui/tinymce-5" : "/tinymce/skins/ui/tinymce-5-dark",
height: props.height,
toolbar_mode: "wrap",
plugins: props.plugins,
toolbar: props.toolbar,
branding: false,
content_css: "/tinymce/skins/content/default/content.css",
setup: (editor) => {
editor.on("init", () => {
editor.getBody().style.fontSize = "14px"
})
editor.ui.registry.addButton("resource", {
text: "资源选择器",
onAction: () => (resourceVisible.value = true)
})
}
})
const editorKey = ref(new Date().getTime())
watch(
() => list.value,
(imgs) => {
let tmp = ""
imgs.map((img) => {
if (
img.indexOf(".jpg") > -1 ||
img.indexOf(".png") > -1 ||
img.indexOf(".bmp") > -1 ||
img.indexOf(".jpeg") > -1 ||
img.indexOf(".svg") > -1 ||
img.indexOf(".gif") > -1
) {
tmp += `<img src=${img} width="100%" />`
}
})
content.value = content.value ? content.value + tmp : tmp
resource.value.clearSelecteds()
resourceVisible.value = false
}
)
watch(
() => content.value,
(vl) => emit("change", vl)
)
</script>

View File

@@ -0,0 +1,73 @@
<template>
<a-modal v-model:visible="modal.visible" :on-before-ok="modal.submit" unmount-on-close @cancel="modal.cancel">
<template #title>
{{ prop.title }}
</template>
<slot name="body"></slot>
<ma-form ref="maFormRef" :columns="prop.column" v-model="form" :options="{ ...options, showButtons: false }" />
</a-modal>
</template>
<script setup>
/**
* 组件 - 弹窗form组件
* 事件
* @visible(show = true|false) 显示关闭事件
*/
import { reactive, ref, watch } from "vue"
import MaForm from "@/components/ma-form/index.vue"
import { Message } from "@arco-design/web-vue"
const emit = defineEmits(["visible", "validateError", "open", "cancel", "close"])
const form = ref({})
const prop = defineProps({
title: { type: String, default: "" }, // 弹出框标题
column: { type: Array, default: [] }, // ma-form字段
default_visible: { type: Boolean, default: false }, // 默认隐藏
options: { type: Object, default: {} }, // ma-form 属性
submit: { type: Function, default: () => {} }
})
const maFormRef = ref()
const modal = reactive({
visible: prop.default_visible,
open(data) {
modal.visible = true
for (let [key, value] of Object.entries(data)) {
form.value[key] = value
}
emit("open", data)
},
close() {
modal.visible = false
},
async submit() {
let validate = await maFormRef.value.validateForm()
if (validate) {
emit("validateError", validate)
return false
}
return prop.submit(form._rawValue)
},
cancel() {
emit("cancel")
}
})
watch(
() => modal.visible,
(vl) => {
emit("visible", modal.visible)
},
{ immediate: true }
)
defineExpose({
open: modal.open,
close: modal.close,
form: form,
formRef: maFormRef
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,54 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-card
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="[props.component?.customClass]"
:extra="props.component?.extra"
:bordered="props.component?.bordered"
:loading="props.component?.loading"
:hoverable="props.component?.hoverable"
:size="props.component?.size"
:header-style="props.component?.headerStyle"
:body-style="props.component?.bodyStyle"
>
<template #title>
<slot :name="`cardTitle-${props.component?.dataIndex ?? ''}`">{{ props.component?.title }}</slot>
</template>
<template #actions>
<slot :name="`cardAction-${props.component?.dataIndex ?? ''}`"></slot>
</template>
<template #cover>
<slot :name="`cardCover-${props.component?.dataIndex ?? ''}`"></slot>
</template>
<template #extra>
<slot :name="`cardExtra-${props.component?.dataIndex ?? ''}`"></slot>
</template>
<template v-for="(component, componentIndex) in props.component?.formList ?? []" :key="componentIndex">
<component :is="getComponentName(component?.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</a-card>
</template>
<script setup>
import { onMounted } from "vue"
import { getComponentName } from "../js/utils.js"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({ component: Object })
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,219 @@
<template>
<a-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:label="props.component.title"
:field="props.component.dataIndex"
:tooltip="props.component.tooltip"
:show-colon="props.component.showColon"
:label-col-flex="props.component.labelColFlex ?? 'auto'"
:label-col-style="{
width: props.component.labelWidth ? props.component.labelWidth : options.labelWidth || '100px'
}"
:rules="props.component.rules || []"
:disabled="props.component.disabled"
:help="props.component.help"
:extra="props.component.extra"
:required="props.component.required"
:hide-label="props.component.hideLabel"
:content-class="props.component.contentClass"
:feedback="props.component.feedback"
:validate-trigger="props.component.validateTrigger"
:validate-status="props.component.validateStatus"
:class="[props.component.customClass]"
>
<a-collapse
:default-active-key="defaultOpenKeys"
expand-icon-position="right"
v-if="(props.component?.type ?? 'group') === 'group'"
:show-expand-icon="false"
>
<a-collapse-item
v-for="(item, itemIndex) in formModel[props.component.dataIndex]"
:key="itemIndex"
:header="`${props.component.title} ${itemIndex + 1}项`"
>
<template #extra>
<a-space>
<a-tooltip content="添加新子项" v-if="!(props.component.hideAdd ?? false)">
<a-button @click.stop="addItem()" type="primary" size="small" shape="round">
<template #icon><icon-plus /></template>
</a-button>
</a-tooltip>
<a-tooltip content="删除该子项" v-if="!(props.component.hideDelete ?? false)">
<a-button
@click.stop="deleteItem(itemIndex)"
:disabled="formModel[props.component.dataIndex].length === 1"
type="primary"
size="small"
shape="round"
status="danger"
>
<template #icon><icon-minus /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
<template v-for="(component, componentIndex) in viewFormList[itemIndex]" :key="componentIndex">
<component
v-if="!containerItems.includes(component.formType)"
:is="getComponentName(component?.formType ?? 'input')"
:component="component"
:customField="getChildrenDataIndex(itemIndex, component.dataIndex)"
>
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</a-collapse-item>
</a-collapse>
<a-table v-else class="w-full" :data="formModel[props.component.dataIndex]" :pagination="false" bordered stripe>
<template #columns id="children-columns">
<!-- 新增删除列 -->
<a-table-column :width="60" fixed="left">
<template #title>
<a-button type="primary" @click="addItem()" size="small" shape="round">
<template #icon>
<icon-plus />
</template>
</a-button>
</template>
<template #cell="{ rowIndex }">
<a-button
type="primary"
status="danger"
size="small"
shape="round"
:disabled="formModel[props.component.dataIndex].length === 1"
@click="deleteItem(rowIndex)"
>
<template #icon><icon-minus /></template>
</a-button>
</template>
</a-table-column>
<a-table-column :width="60" fixed="left">
<template #title>序号</template>
<template #cell="{ rowIndex }"> {{ rowIndex + 1 }} </template>
</a-table-column>
<template v-for="(component, itemIndex) in viewFormList[0]" :key="itemIndex">
<a-table-column
:width="component.width"
:title="component.title ?? '未命名'"
:align="component.align || 'left'"
:fixed="component.fixed"
>
<template #cell="{ rowIndex }">
<component
v-if="!containerItems.includes(component.formType)"
:is="getComponentName(component.formType ?? 'input')"
:component="component"
:customField="getChildrenDataIndex(rowIndex, component.dataIndex)"
>
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</a-table-column>
</template>
</template>
</a-table>
</a-form-item>
</template>
<script setup>
import { ref, inject, provide, onMounted, watch, nextTick, shallowRef, isRef } from "vue"
import { cloneDeep, get, isArray, isUndefined, set } from "lodash"
import { getComponentName, containerItems } from "../js/utils.js"
import { maEvent } from "../js/formItemMixin.js"
import { loadDict, handlerCascader } from "../js/networkRequest.js"
import arrayComponentDefault from "../js/defaultArrayComponent.js"
const props = defineProps({ component: Object })
const formList = props.component.formList
const viewFormList = ref([])
const options = inject("options")
const formModel = inject("formModel")
const dictList = inject("dictList")
const defaultOpenKeys = [0]
if (!formModel.value[props.component.dataIndex]) {
formModel.value[props.component.dataIndex] = []
}
formList.map(async (item) => {
const tmp = cloneDeep(item)
tmp["dataIndex"] = [props.component.dataIndex, tmp.dataIndex].join(".")
await loadDict(dictList.value, tmp)
})
watch(
() => formModel.value[props.component.dataIndex],
(value) => {
if (isArray(value)) {
value.forEach((data, index) => {
if (isArray(data)) {
value[index] = Object.fromEntries(data)
}
viewFormList.value[index] = cloneDeep(formList)
maEvent.customeEvent(props.component, { formList: viewFormList.value[index], data, index }, "onAdd")
})
}
},
{
immediate: true
}
)
if (props.component.type == "table") {
formList.map((item) => {
item["hideLabel"] = true
})
} else {
formModel.value[props.component.dataIndex].map((item, index) => {
if (index > 0) defaultOpenKeys.push(index)
})
}
const addItem = async (data = {}) => {
let index = formModel.value[props.component.dataIndex].length
viewFormList.value[index] = cloneDeep(formList)
maEvent.customeEvent(props.component, { formList: viewFormList.value[index], data, index: index }, "onAdd")
formModel.value[props.component.dataIndex].push(data)
}
const deleteItem = async (index) => {
let res = await maEvent.customeEvent(props.component, { index }, "onDelete")
if (isUndefined(res) || res === true) {
viewFormList.value.splice(index, 1)
await nextTick()
formModel.value[props.component.dataIndex].splice(index, 1)
}
}
const getChildrenDataIndex = (index, dataIndex) => {
return [props.component.dataIndex, index, dataIndex].join(".")
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(async () => {
if (formModel.value[props.component.dataIndex].length === 0) {
for (let i = 0; i < (props.component.emptyRow ?? 1); i++) {
await addItem()
}
}
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>
<style scoped>
:deep(.arco-form-item-content-flex) {
display: block;
}
:deep(.arco-table-cell .arco-form-item) {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,39 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-col
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="[props.component?.customClass]"
:span="props.component?.span ?? 12"
:offset="props.component?.offset"
:order="props.component?.order"
:xs="props.component?.xs"
:sm="props.component?.sm"
:md="props.component?.md"
:lg="props.component?.lg"
:xl="props.component?.xl"
:xxl="props.component?.xxl"
:flex="props.component?.flex"
>
<template v-for="(component, componentIndex) in props.component?.formList ?? []" :key="componentIndex">
<component :is="getComponentName(component?.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</a-col>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue"
import { getComponentName } from "../js/utils.js"
const props = defineProps({ component: Object })
</script>

View File

@@ -0,0 +1,36 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="['grid-responsive-padding', props.component?.customClass]"
:style="props.component?.style"
:span="props.component?.span ?? 12"
>
<template v-for="(component, componentIndex) in props.component?.formList ?? []" :key="componentIndex">
<component :is="getComponentName(component.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</div>
</template>
<script setup>
import { getComponentName } from "../js/utils.js"
const props = defineProps({ component: Object })
</script>
<style>
.grid-responsive-padding {
padding: 0 0.2em;
}
</style>

View File

@@ -0,0 +1,78 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="[gridClass, props.component?.customClass]"
:style="props.component?.style"
>
<template v-for="(col, colIndex) in props.component?.cols ?? []" :key="colIndex">
<ma-grid-tailwind-col :component="col">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-grid-tailwind-col>
</template>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue"
import MaGridTailwindCol from "./grid-tailwind-col.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({ component: Object })
const gridClass = ref(["ma-grid", "grid", "lg:grid-cols-" + props.component?.colNumber ?? 1])
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>
<style scoped>
@media (min-width: 1024px) {
.lg\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.lg\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.lg\:grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.lg\:grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.lg\:grid-cols-9 {
grid-template-columns: repeat(9, minmax(0, 1fr));
}
.lg\:grid-cols-10 {
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.lg\:grid-cols-11 {
grid-template-columns: repeat(11, minmax(0, 1fr));
}
.lg\:grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
</style>

View File

@@ -0,0 +1,40 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-row
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="[props.component?.customClass]"
:gutter="props.component?.gutter"
:justify="props.component?.justify"
:align="props.component?.align"
:div="props.component?.div"
:wrap="props.component?.wrap"
>
<template v-for="(col, colIndex) in props.component?.cols ?? []" :key="colIndex">
<ma-grid-col :component="col">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-grid-col>
</template>
</a-row>
</template>
<script setup>
import { onMounted } from "vue"
import MaGridCol from "./grid-col.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({ component: Object })
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,32 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<td
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="['table-cell', props.component?.customClass]"
:style="props.component?.style"
:colspan="props.component.colSpan"
:rowspan="props.component.rowSpan"
>
<template v-for="(component, componentIndex) in props.component?.formList ?? []" :key="componentIndex">
<component :is="getComponentName(component?.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</td>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue"
import { getComponentName } from "../js/utils.js"
const props = defineProps({ component: Object })
</script>

View File

@@ -0,0 +1,57 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<table
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="['table-container', props.component?.customClass]"
:style="props.component?.style"
>
<tbody>
<tr
v-for="(row, rowIndex) in props.component?.rows ?? []"
:key="rowIndex"
:class="['table-row', row?.customClass]"
:style="row?.style"
>
<template v-for="(col, colIndex) in row.cols ?? []" :key="colIndex">
<ma-table-cell :component="col">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-table-cell>
</template>
</tr>
</tbody>
</table>
</template>
<script setup>
import { onMounted } from "vue"
import MaTableCell from "./table-cell.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({ component: Object })
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>
<style lang="less">
table.table-container {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
.table-cell {
border: 1px solid var(--color-neutral-3);
padding: 0.2em;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-tabs
v-show="typeof props.component?.display == 'undefined' || props.component?.display === true"
:class="[props.component?.customClass]"
:trigger="props.component?.trigger"
:position="props.component?.position"
:size="props.component?.size"
:type="props.component?.type"
:direction="props.component?.direction"
:editable="props.component?.editable"
:animation="props.component?.animation"
:justify="props.component?.justify"
:show-add-button="props.component?.showAddButton"
:hide-content="props.component?.hideContent"
:lazy-load="props.component?.lazyLoad"
:destroy-on-hide="props.component?.destroyOnHide"
@change="maEvent.handleChangeEvent(props.component, $event)"
@tab-click="maEvent.handleTabClickEvent(props.component, $event)"
@add="maEvent.handleTabAddEvent(props.component)"
@delete="maEvent.handleTabDeleteEvent(props.component, $event)"
>
<template #extra>
<slot :name="`tabExtra-${props.component?.dataIndex ?? ''}`"></slot>
</template>
<a-tab-pane
v-for="(tab, index) in props.component?.tabs ?? []"
:key="tab.key ?? index"
:disabled="tab?.disabled"
:closable="tab?.closable"
>
<template #title>
<slot :name="`tabPanelTitle-${props.component?.dataIndex ?? ''}-${tab.key ?? index}`">
{{ tab.title ?? `Tab ${index + 1}` }}
</slot>
</template>
<template v-for="(component, componentIndex) in tab.formList ?? []" :key="componentIndex">
<component :is="getComponentName(component?.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
</a-tab-pane>
</a-tabs>
</template>
<script setup>
import { onMounted } from "vue"
import { getComponentName } from "../js/utils.js"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({ component: Object })
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,68 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-auto-complete
v-model="value"
:disabled="props.component.disabled"
:size="props.component.size"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:readonly="props.component.readonly"
:data="props.component.data ?? []"
:strict="props.component.strict"
:filter-option="props.component.filterOption"
:allow-clear="props.component.allowClear ?? true"
@change="maEvent.customeEvent(props.component, $event, 'onChange')"
@search="maEvent.customeEvent(props.component, $event, 'onSearch')"
@select="maEvent.customeEvent(props.component, $event, 'onSelect')"
@clear="maEvent.customeEvent(props.component, $event, 'onClear')"
>
<slot :name="`autoCompleteFooter-${props.component.dataIndex}`"></slot>
</a-auto-complete>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index, ""))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,43 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-button
:type="props.component.type"
:status="props.component.status"
:size="props.component.size"
:shape="props.component.shape"
:disabled="props.component.disabled"
:long="props.component.long"
:loading="props.component.loading"
:html-type="props.component.htmlType"
:href="props.component.href"
@click="maEvent.handleCommonEvent(props.component, 'onClick')"
>
<template #icon v-if="props.component.icon">
<component :is="props.component.icon" />
</template>
{{ props.component.title ?? "button" }}
</a-button>
</slot>
</template>
<script setup>
import { onMounted } from "vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object
})
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,106 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-cascader
v-model="value"
:path-mode="props.component.pathMode"
:placeholder="props.component.placeholder || `请选择${props.component.title}`"
:allow-clear="props.component.allowClear ?? true"
:allow-search="props.component.allowSearch ?? true"
:size="props.component.size"
:disabled="props.component.disabled"
:multiple="props.component.multiple"
:options="props.component.options ?? dictList[dictIndex] ?? []"
:input-value="props.component.inputValue"
:default-input-value="props.component.defaultInputValue"
:popup-visible="props.component.popupVisible"
:default-popup-visible="props.component.defaultPopupVisible"
:expand-trigger="props.component.expandTrigger"
:filter-option="props.component.filterOption"
:popup-container="props.component.popupContainer"
:max-tag-count="props.component.maxTagCount"
:format-label="props.component.formatLabel"
:trigger-props="props.component.triggerProps"
:check-strictly="props.component.checkStrictly"
:load-more="props.component.loadMore"
:loading="props.component.loading"
:search-option-only-label="props.component.searchOptionOnlyLabel"
:search-delay="props.component.searchDelay"
:field-names="
props.component.fieldNames ?? props.component?.dict?.props ?? { key: 'value', title: 'label' }
"
:value-key="props.component.valueKey"
:fallback="props.component.fallback"
:expand-child="props.component.expandChild"
@change="maEvent.handleChangeEvent(props.component, $event)"
@input-value-change="maEvent.handleInputEvent(props.component, $event)"
@popup-visible-change="maEvent.customeEvent(props.component, $event, 'onPopupVisibleChange')"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
@search="maEvent.customeEvent(props.component, $event, 'onSearch')"
>
</a-cascader>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (
props.component.dict &&
(props.component.dict.name || props.component.dict.data) &&
!value.value &&
!props.component.multiple &&
!props.component.pathMode
) {
value.value = value.value + ""
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,70 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-checkbox-group
v-model="value"
:size="props.component.size"
:max="props.component.max"
:direction="props.component.direction"
:disabled="props.component.disabled"
@change="maEvent.handleChangeEvent(props.component, $event)"
>
<template v-for="(item, index) in dictList[dictIndex] ?? []">
<a-checkbox :value="item.value" :disabled="item.disabled" :indeterminate="item.indeterminate">{{
item.label
}}</a-checkbox>
</template>
</a-checkbox-group>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,55 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-city-linkage v-model="value" :type="props.component.type" :mode="props.component.mode">
</ma-city-linkage>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaCityLinkage from "@/components/ma-cityLinkage/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,62 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-code-editor
v-model="value"
style="width: 100%"
:height="props.component.height"
:isBind="props.component.isBind"
:language="props.component.language"
:readonly="props.component.readonly"
@change="maEvent.handleChangeEvent(props.component, $event)"
>
</ma-code-editor>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaCodeEditor from "@/components/ma-codeEditor/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,54 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-color-picker v-model="value"> </ma-color-picker>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaColorPicker from "@/components/ma-colorPicker/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,47 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<component :is="props.component.dataIndex" :component="props.component" :customField="props.customField" />
</slot>
</ma-form-item>
</template>
<script setup>
import { onMounted, getCurrentInstance, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const app = getCurrentInstance().appContext.app
if (
props.component.formType === "component" &&
props.component.component &&
!app._context.components[props.component.dataIndex]
) {
app.component(props.component.dataIndex, props.component.component)
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,37 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-divider
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:class="[props.component.customClass]"
:margin="props.component.margin"
:direction="props.component.direction"
:orientation="props.component.orientation"
:type="props.component.type"
:size="props.component.size"
>
{{ props.component?.title ?? "" }}
</a-divider>
</slot>
</template>
<script setup>
import { onMounted } from "vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object
})
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,60 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-editor
v-model="value"
style="width: 100%"
:height="props.component.height"
:id="props.component.id"
@change="maEvent.handleChangeEvent(props.component, $event)"
>
</ma-editor>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaEditor from "@/components/ma-editor/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,54 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-icon-picker v-model="value"> </ma-icon-picker>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaIconPicker from "@/components/ma-icon/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,81 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-input-number
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:read-only="props.component.readonly"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:mode="props.component.mode"
:precision="props.component.precision"
:step="props.component.step"
:max="props.component.max"
:min="props.component.min"
:formatter="props.component.formatter"
:parser="props.component.parser"
:model-event="props.component.modelEvent"
@input="maEvent.handleInputEvent(props.component, $event)"
@change="maEvent.handleChangeEvent(props.component, $event)"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
>
<template #suffix v-if="props.component.openSuffix">
<slot :name="`inputSuffix-${props.component.dataIndex}`" />
</template>
<template #prefix v-if="props.component.openPrefix">
<slot :name="`inputPrefix-${props.component.dataIndex}`" />
</template>
</a-input-number>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set, toNumber, isNaN } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(toNumber(get(formModel.value, index)))
watch(
() => get(formModel.value, index),
(vl) => (value.value = toNumber(vl))
)
watch(
() => value.value,
(v) => {
if (isNaN(v)) v = undefined
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,80 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-input-tag
v-model="value"
:input-value="props.component.inputValue"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:max-tag-count="props.component.maxTagCount"
:retain-input-value="props.component.retainInputValue"
:format-tag="props.component.formatTag"
:unique-value="props.component.uniqueValue"
:field-names="props.component.fieldNames"
@input-value-change="maEvent.customeEvent(props.component, $event, 'onInputValueChange')"
@change="maEvent.handleChangeEvent(props.component, $event)"
@remove="maEvent.customeEvent(props.component, $event, 'onRemove')"
@press-enter="maEvent.customeEvent(props.component, $event, 'onPressEnter')"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
>
<template #suffix v-if="props.component.openSuffix">
<slot :name="`inputSuffix-${props.component.dataIndex}`" />
</template>
<template #prefix v-if="props.component.openPrefix">
<slot :name="`inputPrefix-${props.component.dataIndex}`" />
</template>
</a-input-tag>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,102 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<component
:is="getComponentName()"
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:read-only="props.component.readonly"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:max-length="props.component.maxLength"
:show-word-limit="props.component.showWordLimit"
:word-length="props.component.wordLength"
:word-slice="props.component.wordSlice"
:invisible-button="props.component.invisibleButton"
:search-button="props.component.searchButton"
:loading="props.component.invisibleButton"
:button-text="props.component.buttonText"
@input="maEvent.handleInputEvent(props.component, $event)"
@change="maEvent.handleChangeEvent(props.component, $event)"
@press-enter="maEvent.handleCommonEvent(props.component, 'onPressEnter')"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
@search="maEvent.handleInputSearchEvent(props.component, $event)"
>
<template #prepend v-if="props.component.openPrepend">
<slot :name="`inputPrepend-${props.component.dataIndex}`" />
</template>
<template #append v-if="props.component.openAppend">
<slot :name="`inputAppend-${props.component.dataIndex}`" />
</template>
<template #suffix v-if="props.component.openSuffix">
<slot :name="`inputSuffix-${props.component.dataIndex}`" />
</template>
<template #prefix v-if="props.component.openPrefix">
<slot :name="`inputPrefix-${props.component.dataIndex}`" />
</template>
</component>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
const getComponentName = () => {
if (props.component.formType === "input") {
return "a-input"
} else if (props.component.formType === "input-password") {
return "a-input-password"
} else if (props.component.formType === "input-search") {
return "a-input-search"
} else {
return "a-input"
}
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,42 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-form-item
:label="props.component.title"
:field="props.customField ?? props.component.dataIndex"
:tooltip="props.component.tooltip"
:show-colon="props.component.showColon"
:label-col-flex="props.component.labelColFlex ?? 'auto'"
:label-col-style="{
width: props.component.labelWidth ? props.component.labelWidth : options.labelWidth || '100px'
}"
:rules="props.component.rules"
:disabled="props.component.disabled"
:help="props.component.help"
:extra="props.component.extra"
:required="props.component.required"
:hide-label="props.component.hideLabel"
:content-class="props.component.contentClass"
:feedback="props.component.feedback"
:validate-trigger="props.component.validateTrigger ?? 'blur'"
:validate-status="props.component.validateStatus"
:class="[props.component.customClass]"
>
<slot></slot>
</a-form-item>
</template>
<script setup>
import { inject } from "vue"
const options = inject("options")
const props = defineProps({
component: Object,
customField: { type: String, default: null }
})
</script>

View File

@@ -0,0 +1,39 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-link
:status="props.component.status"
:hoverable="props.component.hoverable"
:disabled="props.component.disabled"
:loading="props.component.loading"
:href="props.component.href"
@click="maEvent.handleCommonEvent(props.component, 'onClick')"
>
<template #icon v-if="props.component.icon">
<component :is="props.component.icon" />
</template>
{{ props.component.title ?? "link" }}
</a-link>
</slot>
</template>
<script setup>
import { onMounted } from "vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object
})
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,71 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-mention
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:data="props.component.data ?? []"
:prefix="props.component.prefix"
:split="props.component.split"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:type="props.component.type"
@input="maEvent.handleInputEvent(props.component, $event)"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
@search="maEvent.customeEvent(props.component, $event, 'onSearch')"
@select="maEvent.customeEvent(props.component, $event, 'onSelect')"
>
</a-mention>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,116 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<component
:is="getComponentName()"
v-model="value"
:placeholder="
props.component.formType === 'range'
? ['请选择开始时间', '请选择结束时间']
: `请选择${props.component.title}`
"
:hide-trigger="props.component.hideTrigger"
:allow-clear="props.component.allowClear ?? true"
:format="props.component.format"
:size="props.component.size"
:shortcuts="props.component.shortcuts"
:shortcuts-position="props.component.shortcutsPosition"
:position="props.component.position"
:popup-visible="props.component.popupVisible"
:default-popup-visible="props.component.defaultPopupVisible"
:trigger-props="props.component.triggerProps"
:unmount-on-close="props.component.unmountOnClose"
:disabled="props.component.disabled"
:disabled-input="props.component.disabledInput"
:disabled-date="props.component.disabledDate"
:disabled-time="props.component.disabledTime"
:value-format="props.component.valueFormat"
:readonly="props.component.readonly"
:error="props.component.error"
:show-time="props.component.showTime"
:preview-shortcut="props.component.previewShortcut"
:show-confirm-btn="props.component.showConfirmBtn"
:time-picker-props="
props.component.formType == 'range' ? { defaultValue: ['00:00:00', '23:59:59'] } : {}
"
:separator="props.component.separator"
:mode="props.component.mode"
style="width: 100%"
@change="handlePickerChangeEvent"
@select="handlePickerSelectEvent"
@ok="handlePickerOkEvent"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@popup-visible-change="maEvent.customeEvent(props.component, $event, 'onvVisibleChange')"
@select-shortcut="maEvent.customeEvent(props.component, $event, 'onSelectShortcut')"
@picker-value-change="handlePickerValueChangeEvent"
/>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
const getComponentName = () => {
if (["date", "month", "year", "week", "quarter", "range", "time"].includes(props.component.formType)) {
return `a-${props.component.formType}-picker`
}
}
const handlePickerChangeEvent = (value, date, dateString) => {
maEvent.handleChangeEvent(props.component, { value, date, dateString })
}
const handlePickerSelectEvent = (value, date, dateString) => {
maEvent.customeEvent(props.component, { value, date, dateString }, "onSelect")
}
const handlePickerValueChangeEvent = (value, date, dateString) => {
maEvent.customeEvent(props.component, { value, date, dateString }, "onPickerValueChange")
}
const handlePickerOkEvent = (value, date, dateString) => {
maEvent.customeEvent(props.component, { value, date, dateString }, "onOk")
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,97 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-radio-group
v-model="value"
:size="props.component.size"
:direction="props.component.direction"
:type="props.component.type"
:disabled="props.component.disabled"
@change="handleCascaderChangeEvent($event)"
>
<template v-for="(item, index) in dictList[dictIndex] ?? []">
<a-radio :value="item.value" :disabled="item.disabled">{{ item.label }}</a-radio>
</template>
</a-radio-group>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, nextTick, watch } from "vue"
import MaFormItem from "./form-item.vue"
import { get, set, isUndefined } from "lodash"
import { maEvent } from "../js/formItemMixin.js"
import { handlerCascader } from "../js/networkRequest.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const formLoading = inject("formLoading")
const columns = inject("columns")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index, ""))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (value.value === "") {
value.value = undefined
} else if (
!isUndefined(value.value) &&
props.component.dict &&
(props.component.dict.name || props.component.dict.data)
) {
value.value = value.value + ""
}
const handleCascaderChangeEvent = async (value) => {
formLoading.value = true
const component = props.component
// 执行自定义事件
if (component.onChange) {
maEvent.handleChangeEvent(component, value)
}
// 处理联动
if (!index.match(/^(\w+)\.\d+\./)) {
await handlerCascader(value, component, columns.value, dictList.value, formModel.value)
}
nextTick(() => (formLoading.value = false))
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,65 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-rate
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:count="props.component.count"
:allow-half="props.component.allowHalf"
:grading="props.component.grading"
:color="props.component.color"
@change="maEvent.handleInputEvent(props.component, $event)"
@hover-change="maEvent.customeEvent(props.component, $event, 'onHoverChange')"
>
</a-rate>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,71 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-resource
v-if="(props.component.type ?? 'preview') == 'preview'"
v-model="value"
:multiple="props.component.multiple"
:onlyData="props.component.onlyData"
:returnType="props.component.returnType"
/>
<ma-resource-button
v-else
v-model="value"
:multiple="props.component.multiple"
:onlyData="props.component.onlyData"
:returnType="props.component.returnType"
/>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaResource from "@/components/ma-resource/index.vue"
import MaResourceButton from "@/components/ma-resource/button.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (props.component.multiple && !value.value) {
value.value = []
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,168 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-select
v-model:model-value="value"
:multiple="props.component.multiple"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请选择${props.component.title}`"
:loading="props.component.loading"
:allow-search="props.component.allowSearch ?? true"
:allow-create="props.component.allowCreate"
:max-tag-count="props.component.maxTagCount"
:bordered="props.component.bordered"
:unmount-on-close="props.component.unmountOnClose"
:popup-container="props.component.popupContainer"
:filter-option="props.component.filterOption"
:virtual-list-props="props.component.virtualListProps"
:trigger-props="props.component.triggerProps"
:format-label="props.component.formatLabel"
:fallback-option="props.component.fallbackOption"
:show-extra-options="props.component.showExtraOptions"
:value-key="props.component.valueKey"
:search-delay="props.component.searchDelay"
:limit="props.component.limit"
:field-names="props.component.fieldNames"
:scrollbar="props.component.scrollbar"
@input-value-change="maEvent.handleInputEvent(props.component, $event)"
@change="handleCascaderChangeEvent($event)"
@remove="maEvent.customeEvent(props.component, $event, 'onRemove')"
@popup-visible-change="maEvent.customeEvent(props.component, $event, 'onPopupVisibleChange')"
@dropdown-scroll="maEvent.handleCommonEvent(props.component, 'onDropdownScroll')"
@dropdown-reach-bottom="maEvent.handleCommonEvent(props.component, 'onDropdownReachBottom')"
@exceed-limit="maEvent.customeEvent(props.component, $event, 'onExceedLimit')"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
@search="maEvent.customeEvent(props.component, $event, 'onSearch')"
>
<template v-for="(item, index) in dictList[dictIndex] ?? []">
<a-option :value="item.value" :disabled="item.disabled">{{ item.label }}</a-option>
</template>
<template #header v-if="props.component.multiple">
<div style="padding: 6px 12px">
<a-space>
<a-checkbox :value="false" @change="handleSelectAll">全选/清除</a-checkbox>
<a-button size="mini" type="outline" @click="handleInverse">反选</a-button>
</a-space>
</div>
</template>
<template #footer v-if="props.component?.dict.pageOption ?? false">
<div class="flex justify-center">
<a-pagination class="p-2" size="mini" :total="200" simple>
<template #page-item-step="{ type }">
<div>{{ type === "previous" ? "上一页" : "下一页" }}</div>
</template>
</a-pagination>
</div>
</template>
</a-select>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, nextTick, watch } from "vue"
import MaFormItem from "./form-item.vue"
import { get, isUndefined, set, xor } from "lodash"
import { maEvent } from "../js/formItemMixin.js"
import { handlerCascader } from "../js/networkRequest.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const formLoading = inject("formLoading")
const columns = inject("columns")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index, ""))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (value.value === "") {
value.value = undefined
} else if (
!isUndefined(value.value) &&
props.component.dict &&
(props.component.dict.name || props.component.dict.data) &&
!props.component.multiple
) {
value.value = value.value + ""
}
const handleSelectAll = (status) => {
if (isUndefined(value.value)) {
value.value = []
}
if (status) {
dictList.value[dictIndex].map((item) => {
value.value.push(item.value)
})
} else {
value.value = []
}
}
const handleInverse = () => {
if (isUndefined(value.value)) {
value.value = []
}
const ids = []
dictList.value[dictIndex].map((item) => ids.push(item.value))
value.value = xor(ids, value.value)
}
const handleCascaderChangeEvent = async (value) => {
formLoading.value = true
const component = props.component
// 执行自定义事件
if (component.onChange) {
maEvent.handleChangeEvent(component, value)
}
// 处理联动
if (!index.match(/^(\w+)\.\d+\./)) {
await handlerCascader(value, component, columns.value, dictList.value, formModel.value)
}
nextTick(() => (formLoading.value = false))
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,68 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-slider
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear"
:disabled="props.component.disabled"
:step="props.component.step"
:show-tooltip="props.component.showTooltip"
:range="props.component.range"
:direction="props.component.direction"
:max="props.component.max"
:min="props.component.min"
:marks="props.component.marks"
:show-input="props.component.showInput"
:show-ticks="props.component.showTicks"
@change="maEvent.handleChangeEvent(props.component, $event)"
>
</a-slider>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,33 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<div
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:class="['static-text', props.component.customClass]"
:style="props.component.style"
>
{{ props.component.title ?? "" }}
</div>
</slot>
</template>
<script setup>
import { onMounted } from "vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object
})
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,79 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-switch
v-model="value"
:size="props.component.size"
:disabled="props.component.disabled"
:loading="props.component.loading"
:type="props.component.type"
:checked-value="props.component.checkedValue"
:unchecked-value="props.component.uncheckedValue"
:checked-color="props.component.checkedColor"
:unchecked-color="props.component.uncheckedColor"
:before-change="props.component.beforeChange"
@change="maEvent.handleChangeEvent(props.component, $event)"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
>
<template #checked>
<slot :name="`switchChecked-${props.component.dataIndex}`" />
</template>
<template #unchecked>
<slot :name="`switchUnchecked-${props.component.dataIndex}`" />
</template>
<template #checked-icon>
<slot :name="`switchChecked-${props.component.dataIndex}`" />
</template>
<template #unchecked-icon>
<slot :name="`switchUncheckedIcon-${props.component.dataIndex}`" />
</template>
</a-switch>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,71 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-textarea
v-model="value"
:size="props.component.size"
:allow-clear="props.component.allowClear ?? true"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:auto-size="props.component.autoSize"
:error="props.component.error"
:placeholder="props.component.placeholder ?? `请输入${props.component.title}`"
:max-length="props.component.maxLength"
:show-word-limit="props.component.showWordLimit"
:word-length="props.component.wordLength"
:word-slice="props.component.wordSlice"
@input="maEvent.handleInputEvent(props.component, $event)"
@change="maEvent.handleChangeEvent(props.component, $event)"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@focus="maEvent.handleCommonEvent(props.component, 'onFocus')"
@blur="maEvent.handleCommonEvent(props.component, 'onBlur')"
>
</a-textarea>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,73 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-transfer
v-model="value"
:show-search="props.component.showSearch"
:show-select-all="props.component.showSelectAll"
:title="typeof props.component.title == 'array' ? props.component.title : ['源数据', '目标数据']"
:disabled="props.component.disabled"
:simple="props.component.simple"
:data="props.component.data ?? dictList[dictIndex] ?? []"
:fallback="props.component.fallback"
@change="maEvent.handleChangeEvent(props.component, $event)"
@select="maEvent.customeEvent(props.component, $event, 'onSelect')"
>
</a-transfer>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (props.component.dict && (props.component.dict.name || props.component.dict.data)) {
value.value = value.value + ""
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,101 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-tree-select
v-model="value"
:data="props.component.data ?? dictList[dictIndex] ?? []"
:disabled="props.component.disabled"
:readonly="props.component.readonly"
:loading="props.component.loading"
:error="props.component.error"
:size="props.component.size"
:border="props.component.border"
:allow-search="props.component.allowSearch ?? true"
:allow-clear="props.component.allowClear ?? true"
:placeholder="props.component.placeholder ?? `请选择${props.component.title}`"
:max-tag-count="props.component.maxTagCount"
:multiple="props.component.multiple"
:field-names="
props.component.fieldNames ?? props.component?.dict?.props ?? { key: 'value', title: 'label' }
"
:label-in-value="props.component.labelInValue"
:tree-checkable="props.component.treeCheckable"
:tree-check-strictly="props.component.treeCheckStrictly"
:tree-checked-strategy="props.component.treeCheckedStrategy"
:tree-props="props.component.treeProps"
:trigger-props="props.component.triggerProps"
:popup-visible="props.component.popupVisible"
:default-popup-visible="props.component.defaultPopupVisible"
:dropdown-style="props.component.dropdownStyle"
:dropdown-class-name="props.component.dropdownClassName"
:filter-tree-node="props.component.filterTreeNode"
:load-more="props.component.loadMore"
:disable-filter="props.component.disableFilter"
:popup-container="props.component.popupContainer"
:fallback-option="props.component.fallbackOption"
:selectable="props.component.selectable"
:scrollbar="props.component.scrollbar"
@change="maEvent.handleChangeEvent(props.component, $event)"
@popup-visible-change="maEvent.customeEvent(props.component, $event, 'onPopupVisibleChange')"
@clear="maEvent.handleCommonEvent(props.component, 'onClear')"
@search="maEvent.customeEvent(props.component, $event, 'onSearch')"
>
</a-tree-select>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
import { handlerCascader } from "../js/networkRequest.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const dictList = inject("dictList")
const index = props.customField ?? props.component.dataIndex
const dictIndex = index.match(/^(\w+\.)\d+\./)
? index.match(/^(\w+\.)\d+\./)[1] + props.component.dataIndex
: props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (props.component.dict && (props.component.dict.name || props.component.dict.data) && !props.component.multiple) {
value.value = value.value + ""
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,73 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-upload
v-model="value"
:title="props.component.title"
:disabled="props.component.disabled"
:icon="props.component.icon"
:rounded="props.component.rounded"
:multiple="props.component.multiple"
:draggable="props.component.draggable"
:size="props.component.size"
:chunk="props.component.chunk"
:chunkSize="props.component.chunkSize"
:limit="props.component.limit"
:tip="props.component.tip"
:type="props.component.type"
:accept="props.component.accept"
:returnType="props.component.returnType"
:fileType="props.component.fileType"
:showList="props.component.showList"
:requestData="props.component.requestData"
>
</ma-upload>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaUpload from "@/components/ma-upload/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,63 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-user-select
v-model="value"
:text="props.component.text"
:multiple="props.component.multiple ?? true"
:onlyId="props.component.onlyId"
:isEcho="props.component.isEcho ?? true"
/>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaUserSelect from "@/components/ma-user/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
if (props.component.multiple && !value.value) {
value.value = []
}
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,53 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<ma-user-info v-model="value" :field="props.component.field"> </ma-user-info>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaUserInfo from "@/components/ma-userInfo/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>

View File

@@ -0,0 +1,85 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"
:component="props.component"
:custom-field="props.customField"
>
<slot :name="`form-${props.component.dataIndex}`" v-bind="props.component">
<a-input v-model="value" :placeholder="props.component.placeholder ?? '请输入验证码'" allow-clear>
<template #append>
<ma-verify-code
ref="formVerifyCode"
:height="props.component.height ?? 32"
:width="props.component.width"
:size="props.component.size"
:pool="props.component.pool"
:showError="false"
/>
</template>
</a-input>
</slot>
</ma-form-item>
</template>
<script setup>
import { ref, inject, onMounted, watch } from "vue"
import { get, set } from "lodash"
import MaVerifyCode from "@/components/ma-verifyCode/index.vue"
import MaFormItem from "./form-item.vue"
import { maEvent } from "../js/formItemMixin.js"
const props = defineProps({
component: Object,
customField: { type: String, default: undefined }
})
const formVerifyCode = ref()
const formModel = inject("formModel")
const index = props.customField ?? props.component.dataIndex
const value = ref(get(formModel.value, index))
watch(
() => get(formModel.value, index),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => {
set(formModel.value, index, v)
index.indexOf(".") > -1 && delete formModel.value[index]
}
)
const component = props.component
component.rules = [
{ required: true, message: "请输入验证码" },
{
validator: (value, callback) => {
if (!formVerifyCode.value.checkResult(value)) {
callback("验证码错误")
return false
}
}
}
]
maEvent.handleCommonEvent(props.component, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(props.component, "onMounted")
})
</script>
<style scoped>
:deep(.arco-input-append) {
padding: 0;
}
</style>

View File

@@ -0,0 +1,287 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="w-full">
<a-spin :loading="formLoading" :tip="options.loadingText" class="w-full">
<div
v-if="options.showFormTitle"
:class="['ma-form-title', options.formTitleClass]"
:style="options.formTitleStyle"
>
{{ options.formTitle }}
</div>
<a-form
ref="maFormRef"
:model="form"
:class="['ma-form', options?.customClass]"
:label-align="options?.labelAlign"
:layout="options?.layout"
:size="options?.size"
:disabled="options?.disabled"
:rules="options?.rules"
@submit="formSubmit"
>
<template v-for="(component, componentIndex) in columns" :key="componentIndex">
<component :is="getComponentName(component?.formType ?? 'input')" :component="component">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</component>
</template>
<div class="text-center" v-if="options.showButtons">
<a-space>
<slot name="formBeforeButtons" />
<slot name="formButtons">
<a-button
:type="options.submitType"
:status="options.submitStatus"
v-if="options.submitShowBtn"
html-type="submit"
>
<template #icon v-if="options?.submitIcon">
<component :is="options.submitIcon" />
</template>
{{ options.submitText }}
</a-button>
<a-button
:type="options.resetType"
:status="options.resetStatus"
v-if="options.resetShowBtn"
@click="resetForm"
>
<template #icon v-if="options?.resetIcon">
<component :is="options.resetIcon" />
</template>
{{ options.resetText }}
</a-button>
</slot>
<slot name="formAfterButtons" />
</a-space>
</div>
</a-form>
</a-spin>
</div>
</template>
<script setup>
import { ref, watch, provide, onMounted, nextTick, getCurrentInstance } from "vue"
import { isNil, get } from "lodash"
import defaultOptions from "./js/defaultOptions.js"
import { getComponentName, toHump, interactiveControl, handleFlatteningColumns } from "./js/utils.js"
import { loadDict, handlerCascader } from "./js/networkRequest.js"
import arrayComponentDefault from "./js/defaultArrayComponent.js"
import { maEvent } from "./js/formItemMixin.js"
import { Message } from "@arco-design/web-vue"
const containerList = import.meta.globEager("./containerItem/*.vue")
const componentList = import.meta.globEager("./formItem/*.vue")
const _this = getCurrentInstance().appContext
for (const path in containerList) {
const name = path.match(/([A-Za-z0-9_-]+)/g)[1]
const containerName = `Ma${toHump(name)}`
if (!_this.components[containerName]) {
_this.app.component(containerName, containerList[path].default)
}
}
for (const path in componentList) {
const name = path.match(/([A-Za-z0-9_-]+)/g)[1]
const componentName = `Ma${toHump(name)}`
if (!_this.components[componentName]) {
_this.app.component(componentName, componentList[path].default)
}
}
const formLoading = ref(false)
const maFormRef = ref()
const flatteningColumns = ref([])
const dictList = ref({})
const cascaderList = ref([])
const form = ref({})
const props = defineProps({
modelValue: { type: Object, default: {} },
columns: { type: Array },
options: { type: Object, default: {} }
})
const emit = defineEmits(["onSubmit", "update:modelValue"])
watch(
() => props.modelValue,
(vl) => (form.value = vl),
{ immediate: true, deep: true }
)
watch(
() => form.value,
(vl) => {
interactiveControl(vl, flatteningColumns.value)
emit("update:modelValue", vl)
},
{ deep: true }
)
const options = ref(Object.assign(JSON.parse(JSON.stringify(defaultOptions)), props.options))
// 初始化
const init = async () => {
formLoading.value = true
handleFlatteningColumns(props.columns, flatteningColumns.value)
// 收集数据列表
flatteningColumns.value.map((item) => {
if (item.cascaderItem && item.cascaderItem.length > 0) {
cascaderList.value.push(...item.cascaderItem)
}
})
// 初始化数据
flatteningColumns.value.map(async (item) => {
if (isNil(form.value[item.dataIndex]) && !item.isChildrenForm) {
form.value[item.dataIndex] = undefined
if (arrayComponentDefault.includes(item.formType) && !item.isChildrenForm) {
form.value[item.dataIndex] = []
}
}
// 字典
if (!cascaderList.value.includes(item.dataIndex) && item.dict) {
await loadDict(dictList.value, item)
}
// 联动
await handlerCascader(
get(form.value, item.dataIndex),
item,
flatteningColumns.value,
dictList.value,
form.value,
false
)
})
await nextTick(() => {
interactiveControl(form.value, flatteningColumns.value)
formLoading.value = false
})
}
provide("options", options.value)
provide("columns", flatteningColumns)
provide("dictList", dictList)
provide("formModel", form)
provide("formLoading", formLoading)
maEvent.handleCommonEvent(options.value, "onCreated")
onMounted(() => {
maEvent.handleCommonEvent(options.value, "onMounted")
options.value.init && init()
maEvent.handleCommonEvent(options.value, "onInit")
})
const done = (status) => (formLoading.value = status)
const validateForm = async () => {
const valid = await maFormRef.value.validate()
if (valid) {
let message = ""
for (let name in valid) message += valid[name].message + "、"
Message.error(message.substring(0, message.length - 1))
}
return valid
}
const resetForm = async () => await maFormRef.value.resetFields()
const clearValidate = async () => await maFormRef.value.clearValidate()
const formSubmit = async () => ((await validateForm()) && !formLoading.value) || emit("onSubmit", form.value, done)
const getFormRef = () => maFormRef.value
const getDictList = () => dictList.value
const getDictService = () => {
const DictService = function (dictList) {
/**
* dict项服务类
* @param dataIndex
* @param dictData
* @constructor
*/
const DictItemService = function (dataIndex, dictData) {
this.dict = dictData
this.dataIndex = dataIndex
/**
* 返回原DictData对象
* @returns {*}
*/
this.getRawDictData = () => {
return this.dict
}
/**
* 追加
* @param label
* @param value
* @param extend
*/
this.append = (label, value, extend = {}) => {
this.getRawDictData().push(
Object.assign(
{
label: label,
value: value
},
extend
)
)
}
/**
* 重新加载dict
* @param dictConfig
* @returns {Promise<void>}
*/
this.loadDict = (dictConfig) => {
return loadDict(dictList, { formType: "select", dict: dictConfig, dataIndex: this.dataIndex })
}
}
this.dictMap = new Map()
for (const [dataIndex, dictData] of Object.entries(dictList)) {
this.dictMap.set(dataIndex, new DictItemService(dataIndex, dictData))
}
this.get = (key) => {
return this.dictMap.get(key)
}
}
return new DictService(getDictList())
}
const getColumns = () => flatteningColumns.value
const getCascaderList = () => cascaderList.value
const getFormData = () => form.value
defineExpose({
init,
getFormRef,
getColumns,
getDictList,
getDictService,
getCascaderList,
getFormData,
validateForm,
resetForm,
clearValidate
})
</script>
<style lang="less" scoped>
.ma-form-title {
font-size: 18px;
text-align: center;
}
</style>

View File

@@ -0,0 +1 @@
export default ["checkbox", "user-select", "children-form", "resource"]

View File

@@ -0,0 +1,51 @@
export default {
// 是否自动初始化表单并加载字典及联动远程数据
init: true,
// 表单加载数据中提示文案
loadingText: "加载中...",
// 表单样式class
customClass: [],
// 表单控件尺寸(全局) 'mini' | 'small' | 'medium' | 'large'
size: "medium",
// 标签的对齐方向
labelAlign: "right",
// horizontal 水平排列 vertical 垂直排列 inline 行内排列
layout: "horizontal",
// 表单是否禁用
disabled: false,
// 表单项验证规则整体配置,例子:{ title: [{ required: true, message: '请输入标题'}] }
rules: [],
// 是否显示按钮
showButtons: true,
// 提交按钮图标
submitIcon: "icon-send",
// 提交按钮类型
submitType: "primary",
// 提交按钮状态
submitStatus: "normal",
// 提交按钮文案
submitText: "提交",
// 是否显示提交按钮
submitShowBtn: true,
// 重置按钮图标
resetIcon: "icon-refresh",
// 重置按钮类型
resetType: "secondary",
// 重置按钮状态
resetStatus: "normal",
// 重置按钮文案
resetText: "重置",
// 是否显示重置按钮
resetShowBtn: true,
// 表单标题文案
formTitle: "未命名表单",
// 是否显示表单标题
showFormTitle: false,
// 自定义标题样式css
formTitleStyle: "",
// 自定义标题样式class
formTitleClass: []
}

View File

@@ -0,0 +1,50 @@
import { isString, isFunction } from "lodash"
export const maEvent = {
customeEvent: async (component, value, evName) => {
if (component[evName]) {
if (isFunction(component[evName])) {
return await component[evName](value)
}
if (isString(component[evName])) {
let customFn = new Function("value", component[evName])
return await customFn.call(component, value)
}
}
},
handleCommonEvent: (component, evName) => {
if (component[evName]) {
if (isFunction(component[evName])) {
return component[evName]()
}
if (isString(component[evName])) {
let customFn = new Function("value", component[evName])
return customFn.call(component[evName])
}
}
},
handleInputEvent: (component, value) => {
maEvent.customeEvent(component, value, "onInput")
},
handleChangeEvent: (component, value) => {
maEvent.customeEvent(component, value, "onChange")
},
handleInputSearchEvent: (component, value) => {
maEvent.customeEvent(component, value, "onInputSearch")
},
handleTabClickEvent: (component, value) => {
maEvent.customeEvent(component, value, "onTabClick")
},
handleTabAddEvent: (component) => {
maEvent.customeEvent(component, component?.tabs, "onTabAdd")
},
handleTabDeleteEvent: (component, value) => {
maEvent.customeEvent(component, { tabs: component?.tabs, value }, "onTabDelete")
}
}

View File

@@ -0,0 +1,141 @@
import { isArray, isFunction, set } from "lodash"
import { request } from "@/utils/request"
import commonApi from "@/api/common"
import tool from "@/utils/tool"
export const allowUseDictComponent = [
"radio",
"checkbox",
"select",
"transfer",
"treeSelect",
"tree-select",
"cascader"
]
export const allowCoverComponent = ["radio", "checkbox", "select", "transfer", "cascader"]
export const requestDict = (url, method, params, data, timeout = 10 * 1000) =>
request({ url, method, params, data, timeout })
export const handlerDictProps = (item, tmpArr) => {
let data = []
let tran = {}
let colors = {}
let labelName = "label"
let valueName = "value"
if (item.dict.name && (!item.dict.url || !item.dict.data)) {
labelName = "title"
valueName = "key"
}
if (allowCoverComponent.includes(item.formType)) {
data = tmpArr.map((dicItem) => {
const label = dicItem[(item.dict.props && item.dict.props.label) || labelName]
let tmp = dicItem[(item.dict.props && item.dict.props.value) || valueName]
let disabled =
typeof dicItem["disabled"] == "undefined" ? false : dicItem["disabled"] === true ? true : false
let indeterminate =
typeof dicItem["indeterminate"] == "undefined"
? false
: dicItem["indeterminate"] === true
? true
: false
let value
if (item.dict.name || item.dict.data) value = tmp.toString()
else if (tmp === "true") value = true
else if (tmp === "false") value = false
else value = tmp
tran[value] = label
colors[value] = (item.dict.tagColors && item.dict.tagColors[value]) || undefined
return { label, value, disabled, indeterminate }
})
} else {
data = tmpArr
}
data.tran = tran
data.colors = colors
return data
}
export const loadDict = async (dictList, item) => {
if (allowUseDictComponent.includes(item.formType) && item.dict) {
if (item.dict.name) {
const response = await commonApi.getDict(item.dict.name)
if (response.data) {
dictList[item.dataIndex] = handlerDictProps(item, response.data)
}
} else if (item.dict.url) {
const dictData = tool.local.get("dictData")
if (item.dict.cache && dictData[item.dataIndex]) {
dictList[item.dataIndex] = dictData[item.dataIndex]
} else {
const response = await requestDict(
item.dict.url,
item.dict.method || "GET",
item.dict.params || {},
item.dict.body || {}
)
if (response.data) {
dictList[item.dataIndex] = handlerDictProps(item, response.data)
if (item.dict.cache) {
dictData[item.dataIndex] = dictList[item.dataIndex]
tool.local.set("dictData", dictData)
}
}
}
} else if (item.dict.data) {
if (isArray(item.dict.data)) {
dictList[item.dataIndex] = handlerDictProps(item, item.dict.data)
} else if (isFunction(item.dict.data)) {
const response = await item.dict.data()
dictList[item.dataIndex] = handlerDictProps(item, response)
}
}
}
}
const requestCascaderData = async (val, dict, dictList, name) => {
if (dict && dict.url) {
let response
if (dict && dict.url.indexOf("{{key}}") > 0) {
response = await requestDict(
dict.url.replace("{{key}}", val),
dict.method || "GET",
dict.params || {},
dict.data || {}
)
} else {
let temp = { key: val }
const params = Object.assign(dict.params || {}, temp)
const data = Object.assign(dict.data || {}, temp)
response = await requestDict(dict.url, dict.method || "GET", params || {}, data || {})
}
if (response.data && response.code === 200) {
dictList[name] = response.data.map((dicItem) => {
return {
label: dicItem[(dict.props && dict.props.label) || "label"],
value: dicItem[(dict.props && dict.props.value) || "value"],
disabled:
typeof dicItem["disabled"] == "undefined" ? false : dicItem["disabled"] === true ? true : false,
indeterminate:
typeof dicItem["indeterminate"] == "undefined"
? false
: dicItem["indeterminate"] === true
? true
: false
}
})
} else {
console.error(response)
}
}
}
export const handlerCascader = async (val, column, columns, dictList, formModel, clearData = true) => {
if (column.cascaderItem && isArray(column.cascaderItem)) {
column.cascaderItem.map(async (name) => {
const dict = columns.find((col) => col.dataIndex === name && col.dict).dict
clearData && set(formModel, name, undefined)
requestCascaderData(val, dict, dictList, name)
})
}
}

View File

@@ -0,0 +1,104 @@
import { isEmpty, isFunction, get, set } from "lodash"
export const containerItems = ["tabs", "table", "card", "grid", "grid-tailwind", "children-form"]
export const inputType = ["input", "input-password", "input-search"]
export const pickerType = ["date", "month", "year", "week", "quarter", "range", "time"]
export const interactiveControl = (form, columns) => {
const obj = []
for (let name in form) {
columns.map((item) => {
if (item.dataIndex === name && item.control && isFunction(item.control)) {
obj.push(item.control(get(form, name), form))
}
})
}
obj.map((changItem) => {
columns.map((item, idx) => {
for (let name in changItem) {
if (name === item.dataIndex) {
columns[idx] = Object.assign(item, changItem[name] || {})
}
}
})
})
}
export const upperCaseFirst = (str) => {
if (isEmpty(str)) return ""
return str[0].toUpperCase() + str.substr(1)
}
export const toHump = (str) => {
let temp = str[0].toUpperCase()
let step = 1
while (str.indexOf("-") > -1) {
let index = str.indexOf("-")
temp += str.substring(step, index)
temp += str[index + 1].toUpperCase()
str = str.replace("-", "")
step = index + 1
}
temp += str.substring(step)
return temp
}
export const getComponentName = (formType) => {
if (containerItems.includes(formType)) {
return `Ma${toHump(formType)}`
}
if (pickerType.includes(formType)) {
return "MaFormPicker"
}
if (inputType.includes(formType)) {
return "MaFormInput"
}
return `MaForm${toHump(formType)}`
}
export const handleFlatteningColumns = (data, columns, isChildrenForm = undefined) => {
for (let key in data) {
const item = data[key]
if (containerItems.includes(item.formType)) {
switch (item.formType) {
case "tabs":
if (item.tabs) {
item.tabs.map((tab) => {
tab.formList && handleFlatteningColumns(tab.formList, columns)
})
}
break
case "card":
item.formList && handleFlatteningColumns(item.formList, columns)
break
case "grid-tailwind":
case "grid":
if (item.cols) {
item.cols.map((col) => {
col.formList && handleFlatteningColumns(col.formList, columns)
})
}
break
case "table":
if (item.rows) {
item.rows.map((row) => {
if (row.cols) {
row.cols.map((col) => {
col.formList && handleFlatteningColumns(col.formList, columns)
})
}
})
}
break
// case 'children-form':
// item.formList && handleFlatteningColumns(item.formList, columns, item.dataIndex, true)
// break
}
} else {
// if (isChildrenForm) {
// item['isChildrenForm'] = true
// }
columns.push(item)
}
}
}

View File

@@ -0,0 +1,126 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="w-full">
<a-input-group class="w-full">
<a-input placeholder="请点击右侧按钮选择图标" allow-clear v-model="currentIcon" />
<div class="icon-container" v-if="props.preview">
<component :is="currentIcon" v-if="currentIcon" />
</div>
<a-button type="primary" @click="() => (visible = true)">选择图标</a-button>
</a-input-group>
<a-modal v-model:visible="visible" width="800px" draggable :footer="false">
<template #title>选择图标</template>
<a-tabs class="tabs">
<a-tab-pane key="arco" title="Arco Design">
<ul class="arco">
<li v-for="icon in arcodesignIcons" :key="icon" @click="selectIcon(icon, 'arco')">
<component :is="icon" />
</li>
</ul>
</a-tab-pane>
<a-tab-pane key="mine" title="MineAdmin">
<ul class="mine">
<li v-for="icon in mineadminIcons" :key="icon" @click="selectIcon(icon, 'mine')">
<component :is="icon" />
</li>
</ul>
</a-tab-pane>
</a-tabs>
</a-modal>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from "vue"
import * as arcoIcons from "@arco-design/web-vue/es/icon"
const mineadminIcons = reactive([])
const arcodesignIcons = reactive([])
const visible = ref(false)
const currentIcon = ref()
const props = defineProps({
modelValue: { type: String },
preview: { type: Boolean, default: true }
})
const emit = defineEmits(["update:modelValue"])
onMounted(() => (currentIcon.value = props.modelValue))
for (let icon in arcoIcons) {
arcodesignIcons.push(icon)
}
arcodesignIcons.pop()
const modules = import.meta.globEager("../../assets/ma-icons/*.vue")
for (const path in modules) {
const name = path.match(/([A-Za-z0-9_-]+)/g)[2]
mineadminIcons.push(`MaIcon${name}`)
}
const selectIcon = (icon, className) => {
currentIcon.value = icon
emit("update:modelValue", currentIcon.value)
visible.value = false
}
const handlerChange = (value) => {
selectIcon(value, "")
}
</script>
<style scoped lang="less">
.icon-container {
width: 50px;
height: 32px;
background-color: var(--color-fill-1);
display: flex;
justify-content: center;
align-items: center;
}
.icon {
width: 1em;
fill: var(--color-text-1);
}
.tabs {
ul {
display: flex;
flex-wrap: wrap;
padding-left: 7px;
}
li {
border: 2px solid var(--color-fill-4);
margin-bottom: 10px;
margin-right: 6px;
padding: 5px;
cursor: pointer;
}
li:hover,
li.active {
border: 2px solid rgb(var(--primary-6));
}
& li svg {
width: 2.4em;
height: 2.4em;
}
}
:deep(.arco-select-option-content) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<a-modal
:width="prop.width"
v-model:visible="modal.visible"
:on-before-ok="modal.submit"
:unmount-on-close="true"
@cancel="modal.cancel"
>
<template #title>
{{ prop.title }}
</template>
<slot name="body"></slot>
<a-card class="mt-2" :hoverable="true" v-for="info in layout">
<ma-info
v-if="isArray(info?.child)"
:title="info.title"
:columns="info.child"
:data="infoData"
ref="maInfoRefs"
></ma-info>
<a-descriptions :title="info.title" v-else>
<component :data="infoData" :is="info?.component" />
</a-descriptions>
</a-card>
<ma-form
v-model="form"
ref="maFormRef"
v-if="prop.formColumns.length > 0"
:columns="prop.formColumns"
:options="{ showButtons: false, ...prop.formOptions }"
/>
</a-modal>
</template>
<script setup>
import MaInfo from "../ma-info/index.vue"
import { getCurrentInstance, reactive, ref, watch } from "vue"
import { isArray, isFunction, isObject, isString } from "lodash"
import { isComponent } from "@arco-design/web-vue/es/_utils/vue-utils"
const emit = defineEmits(["visible", "validateError", "open", "cancel", "close"])
const app = getCurrentInstance().appContext.app
const maFormRef = ref()
const prop = defineProps({
width: { type: Number, default: 1200 }, // modal框大小
title: { type: String, default: "" }, // 弹出框标题
column: { type: Array, default: [] }, // ma-form字段
default_visible: { type: Boolean, default: false }, // 默认隐藏
options: { type: Object, default: {} }, // ma-form 属性
submit: { type: Function, default: () => {} },
layout: { type: Object, default: [] },
formOptions: { type: Object, default: {} }, // ma-form-options
formColumns: { type: Array, default: [] }
})
const maInfoRefs = ref([])
const layout = ref([])
const infoData = ref({})
const form = ref({})
watch(
() => prop.layout,
(val) => {
layout.value = val
},
{
immediate: true
}
)
let columnMap = {}
const init = async () => {
let column = prop.column
column.forEach((item) => {
columnMap[item.dataIndex] = item
})
if (layout.value.length > 0) {
layout.value.forEach((item) => {
if (item.component) {
app.component(item.component, item.child)
return
}
item.child.forEach((child, index) => {
if (isString(child)) {
item.child[index] = columnMap[child]
}
})
})
} else {
layout.value.push({
title: "",
child: column
})
}
}
const modal = reactive({
visible: prop.default_visible,
open(data, formData = {}) {
modal.visible = true
for (let [key, value] of Object.entries(data)) {
infoData.value[key] = value
}
for (let [key, value] of Object.entries(formData)) {
form.value[key] = value
}
emit("open", infoData)
},
close() {
modal.visible = false
},
async submit() {
let validate = await maFormRef.value.validateForm()
if (validate) {
emit("validateError", validate)
return false
}
return prop.submit(form._rawValue)
},
cancel() {
emit("cancel")
}
})
init()
defineExpose({
open: modal.open,
close: modal.close,
data: infoData
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,120 @@
<template>
<a-space direction="vertical" size="large" fill>
<a-descriptions
:data="descriptions"
:column="props.column"
:title="props.title"
:layout="props.layout"
:bordered="props.bordered"
table-layout="fixed"
:size="props.size"
:label-style="props.labelStyle"
:value-style="props.valueStyle"
>
<a-descriptions-item v-for="item in descriptions" :label="item.label">
<template v-if="item.formType === 'upload'">
<a-image-preview-group infinite v-if="isArray(item.value)">
<a-space>
<a-image v-for="src in item.value" :src="tool.viewImage(src)" width="50" />
</a-space>
</a-image-preview-group>
<a-image v-else :src="tool.viewImage(item.value)" width="50"></a-image>
</template>
<template v-else-if="item.infoSlot">
<slot :name="item.dataIndex" :row="item" :data="data"></slot>
</template>
<template v-else-if="item.formType === 'radio' || item.formType === 'select'">
<a-tag color="blue">{{ item.dict.data.find((row) => row.value == item.value)?.label }}</a-tag>
</template>
<div v-else>{{ item.value }} {{ item?.textAppend }}</div>
</a-descriptions-item>
</a-descriptions>
</a-space>
</template>
<script setup>
import { getCurrentInstance, inject, provide, ref, watch } from "vue"
import tool from "@/utils/tool"
import { get, isArray, isBoolean, isEmpty, isFunction } from "lodash"
import { loadDict } from "@cps/ma-form/js/networkRequest.js"
import globalColumn from "@/config/column.js"
const dictList = {}
const app = getCurrentInstance().appContext.app
const columns = ref([])
const data = ref({})
const descriptions = ref([])
const dictData = ref([])
const props = defineProps({
columns: { type: Array, default: [] },
data: {},
column: { default: 3 },
title: {
default: ""
},
bordered: {
default: true
},
layout: {
default: "vertical"
},
labelStyle: {
default: {}
},
valueStyle: {
default: {}
},
size: {
default: "large"
}
})
watch(
() => props.columns,
(vl) => {
columns.value = vl
},
{ deep: true, immediate: true }
)
const reset = (vl) => {
data.value = vl
descriptions.value = []
if (!columns.value) {
return
}
columns.value.forEach(async (item) => {
let value = null
if (isEmpty(item) || item.dataIndex === "__operation" || item.infoShow === false) {
return
}
if (isBoolean(item.common) && item.common && globalColumn[item.dataIndex]) {
item = globalColumn[item.dataIndex]
}
if (item.dict) {
await loadDict(dictList, item)
item.dict.data = dictList[item.dataIndex] ?? []
}
if (isFunction(item.customRender)) {
value = item.customRender({ record: data.value })
}
descriptions.value.push({
...item,
label: item.title,
value: value ?? get(data.value, item.dataIndex)
})
})
}
watch(
() => props.data,
(vl) => {
reset(vl)
},
{ deep: true, immediate: true }
)
defineExpose({ reset })
</script>
<style scoped></style>

View File

@@ -0,0 +1,88 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="inline-block">
<a-input-group class="w-full">
<a-trigger position="bottom" auto-fit-position :unmount-on-close="false">
<a-input v-model="inputValue" placeholder="请点击左侧按钮选择资源" readonly v-if="!props.multiple" />
<a-button v-else>预览已选</a-button>
<template #content>
<div class="trigger-content">
<a-empty v-if="list && list.length == 0" />
<a-image :src="inputValue" v-else-if="list && !isArray(list)" />
<div v-else>
<a-image-preview-group infinite>
<a-space>
<a-image :src="item" v-for="(item, index) in list" width="100%" :key="index" />
</a-space>
</a-image-preview-group>
</div>
</div>
</template>
</a-trigger>
<a-button type="primary" @click="visible = true"><icon-experiment /> 资源选择器</a-button>
</a-input-group>
<a-modal v-model:visible="visible" :width="props.width" :footer="false" draggable>
<template #title>资源选择器</template>
<ma-resource v-model="list" :multiple="props.multiple" :only-data="props.onlyData" />
</a-modal>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue"
import MaResource from "@cps/ma-resource/index.vue"
import { isArray } from "lodash"
const list = ref()
const visible = ref(false)
const inputValue = ref("")
const emit = defineEmits(["update:modelValue"])
const props = defineProps({
modelValue: { type: [String, Array] },
multiple: { type: Boolean, default: true },
onlyData: { type: Boolean, default: true },
width: { type: Number, default: 1080 }
})
list.value = props.modelValue
// onMounted(() => )
watch(
() => list.value,
(vl) => {
emit("update:modelValue", list.value)
if (props.multiple) {
console.log(vl)
inputValue.value = isArray(list) ? list.value.join(",") : []
} else {
inputValue.value = list.value
}
visible.value = false
},
{ deep: true }
)
</script>
<style scoped>
.trigger-content {
margin-top: 1px;
background: var(--color-fill-1);
border: 1px solid var(--color-fill-3);
width: 340px;
border-radius: var(--border-radius-medium);
}
:deep(.arco-space) {
display: block;
margin-bottom: 5px;
}
</style>

View File

@@ -0,0 +1,300 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="w-full p-2 resource-container h-full lg:flex lg:justify-between rounded-sm">
<a-modal v-model:visible="openNetworkModal" :ok-text="$t('sys.save')" :on-before-ok="saveNetworkImg" draggable>
<template #title>{{ $t("maResource.saveNetworkImage") }}</template>
<a-input v-model="networkImg" class="mb-3" :placeholder="$t('maResource.networkImageNotice')" allow-clear />
<a-image :src="networkImg" width="100%" style="min-height: 150px" />
</a-modal>
<div class="lg:w-1/5 w-full p-2 shadow">
<ma-tree-slider
v-model="sliderData"
:search-placeholder="$t('maResource.searchResource')"
:field-names="{ title: 'title', key: 'key' }"
@click="handlerClick"
icon="icon-folder"
:selected-keys="['all']"
/>
</div>
<div class="w-full lg:ml-3 mt-3 lg:mt-2 flex flex-col">
<div class="lg:flex lg:justify-between">
<div class="flex">
<ma-upload v-model="uploadFile" multiple :show-list="false" type="chunk" :resource="false" />
<a-button class="ml-3" @click="openNetworkModal = true">
<icon-image /> {{ $t("maResource.saveNetworkImage") }}
</a-button>
</div>
<a-input
v-model="filename"
class="input-search lg:mt-0 mt-2"
:placeholder="$t('maResource.searchFileNotice')"
@press-enter="searchFile"
/>
</div>
<a-spin :loading="resourceLoading" :tip="$t('maResource.loadingText')" class="h-full">
<div class="resource-list mt-2" ref="rl" v-if="attachmentList && attachmentList.length > 0">
<div
class="item rounded-sm"
v-for="(item, index) in attachmentList"
:key="item.hash"
@click="selectFile(item, index)"
>
<img
:src="
!/^(http|https)/g.test(item.url)
? $tool.attachUrl(item.url, getStoreMode(item.storage_mode))
: item.url
"
v-if="item.mime_type.indexOf('image') !== -1"
/>
<div v-else class="text-3xl w-full h-full flex items-center justify-center">
{{ `.${item.suffix}` }}
</div>
<a-tooltip position="bottom">
<div class="file-name">
{{ item.origin_name }}
</div>
<template #content>
<div>存储名称{{ item.object_name }}</div>
<div>存储目录{{ item.storage_path }}</div>
<div>上传时间{{ item.created_at }}</div>
<div>文件大小{{ item.size_info }}</div>
<div>存储模式{{ getStoreMode(item.storage_mode) }}</div>
</template>
</a-tooltip>
</div>
</div>
<a-empty v-else class="mt-10" />
</a-spin>
<div class="lg:flex lg:justify-between">
<a-pagination
:total="pageInfo.total"
v-model:current="pageInfo.currentPage"
v-model:page-size="pageSize"
@change="changePage"
/>
<a-button type="primary" @click="selectComplete" class="mt-3 lg:mt-0">{{
$t("maResource.ok")
}}</a-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue"
import MaUpload from "@cps/ma-upload/index.vue"
import uploadConfig from "@/config/upload"
import MaTreeSlider from "@cps/ma-treeSlider/index.vue"
import commonApi from "@/api/common"
import tool from "@/utils/tool"
import { useI18n } from "vue-i18n"
import { Message } from "@arco-design/web-vue"
const { t } = useI18n()
const sliderData = ref([])
const uploadFile = ref()
const attachmentList = ref([])
const openNetworkModal = ref(false)
const networkImg = ref()
const pageInfo = ref({
total: 1,
currentPage: 1
})
const resourceLoading = ref(true)
const pageSize = ref(30)
const filename = ref()
const selecteds = ref()
const rl = ref()
const emit = defineEmits(["update:modelValue"])
const props = defineProps({
modelValue: { type: [String, Array] },
multiple: { type: Boolean, default: true },
onlyData: { type: Boolean, default: true },
returnType: { type: String, default: "url" }
})
onMounted(async () => {
const treeData = await commonApi.getDict("attachment_type")
treeData.data.unshift({ title: "所有", key: "all" })
sliderData.value = treeData.data
await getAttachmentList({ pageSize: 1 })
if (props.multiple) {
selecteds.value = []
}
})
const getStoreMode = (mode) => {
return uploadConfig.storageMode[mode.toString()]
}
const getAttachmentList = async (params = {}) => {
const requestParams = Object.assign(params, { pageSize: pageSize.value })
resourceLoading.value = true
const response = await commonApi.getResourceList(requestParams)
pageInfo.value = response?.data?.pageInfo
attachmentList.value = response?.data?.items
resourceLoading.value = false
}
const handlerItemClick = (e) => {
console.log(e)
}
const handlerClick = async (item) => {
const type = item[0] === "all" ? undefined : item[0]
await getAttachmentList({ mime_type: type })
}
const searchFile = async () => {
await getAttachmentList({ origin_name: filename.value })
}
const selectFile = (item, index) => {
if (!props.multiple && selecteds.value) {
if (props.onlyData && item.url != selecteds.value) return
if (!props.onlyData && item.id != selecteds.value.id) return
}
const children = rl.value.children
const className = "item rounded-sm"
if (!/^(http|https)/g.test(item.url)) {
item.url = tool.attachUrl(item.url, getStoreMode(item.storage_mode))
}
if (children[index].className.indexOf("active") !== -1) {
children[index].className = className
if (props.multiple) {
selecteds.value.map((file, idx) => {
if (props.onlyData && file == item.url) selecteds.value.splice(idx, 1)
if (!props.onlyData && file.id == item.id) selecteds.value.splice(idx, 1)
})
} else {
selecteds.value = ""
}
} else {
children[index].className = className + " active"
if (props.multiple) {
selecteds.value.push(props.onlyData ? item[props.returnType] : item)
} else {
selecteds.value = props.onlyData ? item[props.returnType] : item
}
}
}
const clearSelecteds = () => {
if (rl.value && rl.value.children) {
const children = rl.value.children
for (let i = 0; i < children.length; i++) {
children[i].className = "item rounded-sm"
}
}
if (props.multiple) {
selecteds.value = []
} else {
selecteds.value = undefined
}
}
const selectComplete = () => {
const files = props.multiple ? Object.assign([], selecteds.value) : selecteds.value
emit("update:modelValue", files)
}
const changePage = async (page) => {
await getAttachmentList({ page })
}
const saveNetworkImg = async (done) => {
if (!networkImg.value) {
Message.error(t("maResource.networkImageNotice"))
done(false)
return
}
const response = await commonApi.saveNetWorkImage({ url: networkImg.value })
if (response.success) {
Message.success(response.message)
await getAttachmentList()
networkImg.value = undefined
done(true)
} else {
Message.error(response.message)
done(false)
}
}
watch(
() => uploadFile,
async () => await getAttachmentList(),
{ deep: true }
)
defineExpose({ clearSelecteds })
</script>
<style scoped lang="less">
.input-search {
width: 252px;
}
.resource-list {
display: flex;
width: 100%;
flex-wrap: wrap;
flex-direction: row;
align-content: center;
.item {
width: 130px;
height: 130px;
border: 2px solid var(--color-fill-1);
margin-right: 10px;
margin-bottom: 20px;
background-color: var(--color-fill-1);
cursor: pointer;
position: relative;
.file-name {
position: absolute;
bottom: 0px;
height: 23px;
width: 100%;
background: rgba(100, 100, 100, 0.3);
line-height: 23px;
font-size: 12px;
overflow: hidden;
padding: 0 10px;
text-align: center;
text-overflow: ellipsis;
color: #fff;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.item:hover {
border: 2px solid rgb(var(--primary-6));
}
.item.active {
border: 2px solid rgb(var(--primary-6));
}
.item.active::after {
content: "";
position: absolute;
width: 126px;
height: 126px;
z-index: 2;
top: 0;
background: rgba(var(--primary-5), 0.2);
}
}
</style>

View File

@@ -0,0 +1,115 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="flex flex-col w-full h-full">
<a-input-group class="mb-2 w-full" size="mini">
<a-input :placeholder="props?.searchPlaceholder" allow-clear @input="changeKeyword" @clear="resetData" />
<a-button
@click="
() => {
isExpand ? maTree.expandAll(false) : maTree.expandAll(true)
isExpand = !isExpand
}
"
>{{ isExpand ? "折叠" : "展开" }}</a-button
>
</a-input-group>
<a-tree
blockNode
:data="treeData"
class="h-full w-full"
@select="handlerSelect"
:field-names="props.fieldNames"
v-model:selected-keys="selectedKeys"
ref="maTree"
v-bind="$attrs"
>
<template #icon v-if="props.icon"><component :is="props.icon" /></template>
</a-tree>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from "vue"
const treeData = ref([])
const selectedKeys = ref([])
const maTree = ref()
const isExpand = ref(false)
const emit = defineEmits(["click"])
const props = defineProps({
modelValue: { type: Array },
searchPlaceholder: { type: String },
selectedKeys: { type: Array },
fieldNames: {
type: Object,
default: () => {
return { title: "label", value: "code" }
}
},
icon: { type: String, default: undefined }
})
onMounted(() => (selectedKeys.value = props.selectedKeys))
watch(
() => props.modelValue,
(val) => {
treeData.value = val
}
)
const handlerSelect = (item, data) => {
selectedKeys.value = [item]
emit("click", ...[item, data])
}
const resetData = () => (treeData.value = props.modelValue)
const changeKeyword = (keyword) => {
if (!keyword || keyword === "") {
treeData.value = Object.assign(props.modelValue, [])
return false
}
treeData.value = searchNode(keyword)
}
const searchNode = (keyword) => {
const loop = (data) => {
let tree = []
data.map((item) => {
if (item.children && item.children.length > 0) {
const temp = loop(item.children)
tree.push(...temp)
} else if (item[props.fieldNames["title"]].indexOf(keyword) !== -1) {
tree.push(item)
}
return tree
})
return tree
}
return loop(props.modelValue)
}
defineExpose({ maTree })
</script>
<style scoped lang="less">
:deep(.arco-tree-node:hover) {
background-color: var(--color-fill-2);
border-radius: 3px;
}
:deep(.arco-tree-node-switcher) {
margin-left: 2px;
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<a-upload
:custom-request="chunkUpload"
:show-file-list="false"
:multiple="config.multiple"
:accept="config.accept"
:disabled="config.disabled"
:tip="config.tip"
:draggable="config.draggable"
:limit="config.limit"
>
<template #upload-button v-if="config.draggable">
<slot name="customer">
<div
style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)"
class="rounded text-center p-7 w-full"
>
<div>
<icon-upload class="text-5xl text-gray-400" />
<div class="text-red-600 font-bold">
{{ config.title === "buttonText" ? $t("upload.buttonText") : config.title }}
</div>
{{ $t("upload.uploadDesc") }}<span style="color: #3370ff">{{ $t("upload.clickUpload") }}</span>
</div>
</div>
</slot>
</template>
</a-upload>
<!-- 单文件 -->
<div class="file-list mt-2" v-if="!config.multiple && config.showList && currentItem?.percent">
<a-progress
v-if="currentItem.percent < 100"
:percent="currentItem.percent"
animation
type="circle"
size="mini"
class="progress"
/>
<a-button class="delete" @click="removeSignFile()" v-if="currentItem.percent === 100">
<template #icon><icon-delete /></template>
</a-button>
<div
v-if="currentItem?.url && currentItem.percent === 100 && currentItem?.status === 'complete'"
class="file-item"
>
{{ currentItem.url }}
</div>
</div>
<!-- 多文件 -->
<div v-if="config.showList" class="file-list mt-2" v-for="(file, idx) in showFileList" :key="idx">
<a-progress
v-if="file.percent < 100"
:percent="file.percent"
animation
type="circle"
size="mini"
class="progress"
/>
<a-button class="delete" @click="removeFile(idx)" v-if="file.percent === 100">
<template #icon><icon-delete /></template>
</a-button>
<div v-if="file?.url && file.percent === 100 && file?.status === 'complete'" class="file-item">
{{ file.url }}
</div>
</div>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import commonApi from "@/api/common"
import tool from "@/utils/tool"
import { isArray } from "lodash"
import { getFileUrl } from "../js/utils"
import { Message } from "@arco-design/web-vue"
import file2md5 from "file2md5"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const props = defineProps({
modelValue: { type: [String, Number, Array], default: () => {} }
})
const emit = defineEmits(["update:modelValue"])
const config = inject("config")
const storageMode = inject("storageMode")
const showFileList = ref([])
const signFile = ref()
const currentItem = ref({})
const chunkUpload = async (options) => {
let idx
if (config.multiple) {
showFileList.value.push(options.fileItem)
idx = showFileList.value.length - 1
} else {
currentItem.value = options.fileItem
}
try {
const file = options.fileItem.file
const hash = await file2md5(file)
if (!file.type) {
Message.error("获取文件类型失败,无法上传")
return
}
const chunks = Math.ceil(file.size / config.chunkSize)
for (let currentChunk = 0; currentChunk < chunks; currentChunk++) {
const start = currentChunk * config.chunkSize
const end = start + config.chunkSize >= file.size ? file.size : start + config.chunkSize
const dataForm = new FormData()
dataForm.append("package", file.slice(start, end))
dataForm.append("hash", hash)
dataForm.append("total", chunks)
dataForm.append("name", file.name)
dataForm.append("type", file.type)
dataForm.append("size", file.size)
dataForm.append("index", currentChunk + 1)
dataForm.append("ext", /[^.]+$/g.exec(file.name)[0])
for (let name in config.requestData) {
dataForm.append(name, config.requestData[name])
}
const res = await commonApi.chunkUpload(dataForm)
if (res.data && res.data.hash) {
res.data.url = tool.attachUrl(res.data.url, storageMode[res.data.storage_mode])
if (config.multiple) {
showFileList.value[idx].percent = 100
showFileList.value[idx].status = "complete"
showFileList.value[idx].url = res.data.url
showFileList.value[idx][config.returnType] = res.data[config.returnType]
let files = []
files = showFileList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
} else {
signFile.value = res.data[config.returnType]
emit("update:modelValue", signFile.value)
currentItem.value.url = res.data.url
currentItem.value.percent = 99
setTimeout(() => {
currentItem.value.status = "complete"
currentItem.value.percent = 100
}, 1000)
}
return
}
if (res.data && res.data.code && res.data.code === 201) {
const percent = parseFloat((1 / chunks).toFixed(2))
if (config.multiple) {
showFileList.value[idx].percent += percent
} else {
currentItem.value.status = "uploading"
currentItem.value.percent += percent
}
}
}
} catch (e) {
console.error(e)
Message.error(t("upload.fileHashFail"))
}
}
const removeSignFile = () => {
currentItem.value = {}
signFile.value = undefined
emit("update:modelValue", null)
}
const removeFile = (idx) => {
showFileList.value.splice(idx, 1)
let files = []
files = showFileList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
}
const init = async () => {
if (config.multiple) {
if (isArray(props.modelValue) && props.modelValue.length > 0) {
const result = await props.modelValue.map(async (item) => {
return await getFileUrl(config.returnType, item, storageMode)
})
const results = await Promise.all(result)
showFileList.value = results.map((item) => {
return {
...item,
percent: 100,
status: "complete"
}
})
} else {
showFileList.value = []
}
} else if (props.modelValue) {
signFile.value = props.modelValue
getFileUrl(config.returnType, props.modelValue, storageMode).then((item) => (currentItem.value.url = item))
currentItem.value.percent = 100
currentItem.value.status = "complete"
} else {
removeSignFile()
}
}
watch(
() => props.modelValue,
(val) => {
init()
},
{
deep: true,
immediate: true
}
)
</script>
<style lang="less" scoped>
.file-list {
position: relative;
background-color: var(--color-primary-light-1);
border-radius: 4px;
height: 36px;
line-height: 36px;
padding: 0 10px;
width: 100%;
.delete {
position: absolute;
z-index: 99;
right: 2px;
top: 2px;
}
.progress {
position: absolute;
left: 30px;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<div class="upload-file w-full">
<a-upload
:custom-request="uploadFileHandler"
:show-file-list="false"
:multiple="config.multiple"
:accept="config.accept"
:disabled="config.disabled"
:tip="config.tip"
:draggable="config.draggable"
:limit="config.limit"
>
<template #upload-button v-if="config.draggable">
<slot name="customer">
<div
style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)"
class="rounded text-center p-7 w-full"
>
<div>
<icon-upload class="text-5xl text-gray-400" />
<div class="text-red-600 font-bold">
{{ config.title === "buttonText" ? $t("upload.buttonText") : config.title }}
</div>
将文件拖到此处<span style="color: #3370ff">点击上传</span>
</div>
</div>
</slot>
</template>
</a-upload>
</div>
<!-- 单文件 -->
<div class="file-list mt-2" v-if="!config.multiple && currentItem?.url && config.showList">
<a-button class="delete" @click="removeSignFile()">
<template #icon><icon-delete /></template>
</a-button>
<div class="file-item">
{{ currentItem.url }}
</div>
</div>
<!-- 多文件 -->
<div v-if="config.showList" class="file-list mt-2" v-for="(file, idx) in showFileList" :key="idx">
<a-button class="delete" @click="removeFile(idx)">
<template #icon><icon-delete /></template>
</a-button>
<div class="file-item">
{{ file.url }}
</div>
</div>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import tool from "@/utils/tool"
import { isArray } from "lodash"
import { getFileUrl, uploadRequest } from "../js/utils"
import { Message } from "@arco-design/web-vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const props = defineProps({
modelValue: { type: [String, Number, Array], default: () => {} }
})
const emit = defineEmits(["update:modelValue"])
const config = inject("config")
const storageMode = inject("storageMode")
const showFileList = ref([])
const signFile = ref()
const currentItem = ref({})
const uploadFileHandler = async (options) => {
if (!options.fileItem) return
if (!config.multiple) {
currentItem.value = options.fileItem
}
const file = options.fileItem.file
if (file.size > config.size) {
Message.warning(file.name + " " + t("upload.sizeLimit"))
currentItem.value = {}
return
}
const result = await uploadRequest(file, "file", "uploadFile", config.requestData)
if (result) {
result.url = tool.attachUrl(result.url, storageMode[result.storage_mode])
if (!config.multiple) {
signFile.value = result[config.returnType]
emit("update:modelValue", signFile.value)
currentItem.value.url = result.url
} else {
showFileList.value.push(result)
let files = []
files = showFileList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
}
}
}
const removeSignFile = () => {
currentItem.value = {}
signFile.value = undefined
emit("update:modelValue", null)
}
const removeFile = (idx) => {
showFileList.value.splice(idx, 1)
let files = []
files = showFileList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
}
const init = async () => {
if (config.multiple) {
if (isArray(props.modelValue) && props.modelValue.length > 0) {
const result = await props.modelValue.map(async (item) => {
return await getFileUrl(config.returnType, item, storageMode)
})
showFileList.value = await Promise.all(result)
} else {
showFileList.value = []
}
} else if (props.modelValue) {
signFile.value = props.modelValue
getFileUrl(config.returnType, props.modelValue, storageMode).then((item) => (currentItem.value.url = item))
currentItem.value.percent = 100
currentItem.value.status = "complete"
} else {
removeSignFile()
}
}
watch(
() => props.modelValue,
(val) => {
init()
},
{
deep: true,
immediate: true
}
)
</script>
<style lang="less" scoped>
.file-list {
position: relative;
background-color: var(--color-primary-light-1);
border-radius: 4px;
height: 36px;
line-height: 36px;
padding: 0 10px;
width: 100%;
.delete {
position: absolute;
z-index: 99;
right: 2px;
top: 2px;
}
.progress {
position: absolute;
left: 30px;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="upload-image flex">
<!-- 单图 -->
<div
:class="'image-list ' + (config.rounded ? 'rounded-full' : '')"
v-if="!config.multiple && currentItem?.url && config.showList"
>
<a-button class="delete" @click="removeSignImage()">
<template #icon><icon-delete /></template>
</a-button>
<a-image width="130" height="130" :class="config.rounded ? 'rounded-full' : ''" :src="currentItem.url" />
</div>
<!-- 多图显示 -->
<a-space v-else-if="config.multiple && config.showList" :class="showImgList.length > 0 ? 'mr-2' : ''" wrap>
<div
:class="'image-list ' + (config.rounded ? 'rounded-full' : '')"
v-for="(image, idx) in showImgList"
:key="idx"
>
<a-button class="delete" @click="removeImage(idx)">
<template #icon><icon-delete /></template>
</a-button>
<a-image width="130" height="130" :class="config.rounded ? 'rounded-full' : ''" :src="image.url" />
</div>
</a-space>
<a-upload
:custom-request="uploadImageHandler"
:show-file-list="false"
:multiple="config.multiple"
:accept="config.accept ?? '.jpg,jpeg,.gif,.png,.svg,.bpm'"
:disabled="config.disabled"
:tip="config.tip"
:limit="config.limit"
>
<template #upload-button>
<slot name="customer">
<div
:class="'upload-skin ' + (config.rounded ? 'rounded-full' : 'rounded-sm')"
v-if="!props.modelValue || config.multiple"
>
<div class="icon text-3xl"><component :is="config.icon" /></div>
<div class="title">
{{ config.title === "buttonText" ? $t("upload.buttonText") : config.title }}
</div>
</div>
</slot>
</template>
</a-upload>
</div>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import tool from "@/utils/tool"
import { isArray } from "lodash"
import { getFileUrl, uploadRequest } from "../js/utils"
import { Message } from "@arco-design/web-vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const props = defineProps({
modelValue: { type: [String, Number, Array], default: () => {} }
})
const emit = defineEmits(["update:modelValue"])
const config = inject("config")
const storageMode = inject("storageMode")
const showImgList = ref([])
const signImage = ref()
const currentItem = ref({})
const uploadImageHandler = async (options) => {
if (!options.fileItem) return
if (!config.multiple) {
currentItem.value = options.fileItem
}
const file = options.fileItem.file
if (file.size > config.size) {
Message.warning(file.name + " " + t("upload.sizeLimit"))
currentItem.value = {}
return
}
const result = await uploadRequest(file, "image", "uploadImage", config.requestData)
if (result) {
result.url = tool.attachUrl(result.url, storageMode[result.storage_mode])
if (!config.multiple) {
signImage.value = result[config.returnType]
emit("update:modelValue", signImage.value)
} else {
showImgList.value.push(result)
let files = []
files = showImgList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
}
}
}
const removeSignImage = () => {
currentItem.value = {}
signImage.value = undefined
emit("update:modelValue", null)
}
const removeImage = (idx) => {
showImgList.value.splice(idx, 1)
let files = []
files = showImgList.value.map((item) => {
return item[config.returnType]
})
emit("update:modelValue", files)
}
const init = async () => {
if (config.multiple) {
if (isArray(props.modelValue) && props.modelValue.length > 0) {
const result = await props.modelValue.map(async (item) => {
return await getFileUrl(config.returnType, item, storageMode)
})
showImgList.value = await Promise.all(result)
} else {
showImgList.value = []
}
} else if (props.modelValue) {
signImage.value = props.modelValue
getFileUrl(config.returnType, props.modelValue, storageMode).then((item) => (currentItem.value.url = item))
currentItem.value.percent = 100
currentItem.value.status = "complete"
} else {
removeSignImage()
}
}
watch(
() => props.modelValue,
(val) => {
init()
},
{
deep: true,
immediate: true
}
)
</script>
<style lang="less" scoped>
.upload-skin {
background-color: var(--color-fill-2);
border: 1px dashed var(--color-fill-4);
width: 130px;
height: 130px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon,
.title {
color: var(--color-text-3);
}
}
.image-list {
cursor: pointer;
position: relative;
background-color: var(--color-fill-2);
width: 130px;
height: 130px;
.delete {
position: absolute;
z-index: 99;
right: 3px;
top: 3px;
display: none;
}
.progress {
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
.image-list:hover {
.delete {
display: block;
}
}
.upload-skin:hover {
border: 1px dashed rgb(var(--primary-6));
}
</style>

View File

@@ -0,0 +1,72 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div>
<ma-image-upload v-if="props.type === 'image'" v-model="file" />
<ma-file-upload v-if="props.type === 'file'" v-model="file" />
<ma-chunk-upload v-if="props.type === 'chunk'" v-model="file" />
</div>
</template>
<script setup>
import { ref, watch, provide } from "vue"
import { Message } from "@arco-design/web-vue"
import uploadConfig from "@/config/upload"
import MaImageUpload from "./components/image-upload.vue"
import MaFileUpload from "./components/file-upload.vue"
import MaChunkUpload from "./components/chunk-upload.vue"
const emit = defineEmits(["update:modelValue"])
const file = ref()
const props = defineProps({
modelValue: { type: [String, Number, Array], default: () => {} },
title: { type: String, default: "buttonText" },
icon: { type: String, default: "icon-plus" },
rounded: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
draggable: { type: Boolean, default: false },
size: { type: Number, default: 4 * 1024 * 1024 },
chunk: { type: Boolean, default: false },
chunkSize: { type: Number, default: 1 * 1024 * 1024 },
limit: { type: Number, default: 0 },
tip: { type: String, default: undefined },
type: { type: String, default: "image" },
accept: { type: String, default: "*" },
returnType: { type: String, default: "url" },
fileType: { type: String, default: "button" },
showList: { type: Boolean, default: true },
requestData: { type: Object, default: {} }
})
if (!["id", "url", "hash"].includes(props.returnType)) {
Message.error("MaUpload组件props的returnType只能为id, url, hash 其中一个")
console.error("MaUpload组件props的returnType只能为id, url, hash 其中一个")
}
watch(
() => props.modelValue,
(val) => {
file.value = val
},
{
deep: true,
immediate: true
}
)
provide("storageMode", uploadConfig.storageMode)
provide("config", props)
watch(
() => file.value,
(vl) => emit("update:modelValue", vl)
)
</script>

View File

@@ -0,0 +1,36 @@
import commonApi from "@/api/common"
import tool from "@/utils/tool"
import file2md5 from "file2md5"
export const getFileUrl = async (returnType, value, storageMode) => {
if (returnType === "url") {
return value
} else if (returnType === "id") {
const { data } = await commonApi.getFileInfoById(value)
if (data) {
data.url = tool.attachUrl(data.url, storageMode[data.storage_mode])
return data
}
return ""
} else if (returnType === "hash") {
const { data } = await commonApi.getFileInfoByHash(value)
if (data) {
data.url = tool.attachUrl(data.url, storageMode[data.storage_mode])
return data
}
return ""
}
}
export const uploadRequest = async (file, type, method, requestData = {}) => {
const hash = await file2md5(file)
const dataForm = new FormData()
dataForm.append(type, file)
dataForm.append("isChunk", false)
dataForm.append("hash", hash)
for (let name in requestData) {
dataForm.append(name, requestData[name])
}
const response = await commonApi[method](dataForm)
return response.data
}

View File

@@ -0,0 +1,142 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="ma-content-block">
<a-space class="flex">
<a-button type="primary" @click="visible = true">
<template #icon><icon-select-all /></template>{{ props.text }}
</a-button>
<a-tag size="large" color="blue" v-if="props.isEcho"
>已选择 {{ isArray(selecteds) ? selecteds.length : 0 }} </a-tag
>
<a-input-tag
v-model="userList"
v-if="props.isEcho"
:style="{ width: '320px' }"
:placeholder="'请点击前面按钮' + props.text"
:max-tag-count="3"
disabled
/>
</a-space>
<a-modal v-model:visible="visible" width="1000px" draggable :on-before-ok="close" unmountOnClose>
<template #title>{{ props.text }}</template>
<ma-crud
ref="crudRef"
:options="crud"
:columns="columns"
v-model:selected-keys="selecteds"
@selection-change="selectHandler"
/>
</a-modal>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue"
import commonApi from "@/api/common"
import { Message } from "@arco-design/web-vue"
import { isArray, isEmpty } from "lodash"
const props = defineProps({
modelValue: { type: Array },
isEcho: { type: Boolean, default: false },
multiple: { type: Boolean, default: true },
onlyId: { type: Boolean, default: true },
text: { type: String, default: "选择用户" }
})
const emit = defineEmits(["update:modelValue", "success"])
const visible = ref(false)
const selecteds = ref([])
const userList = ref([])
onMounted(() => {
if (props.isEcho && props.onlyId) selecteds.value = props.modelValue
})
watch(
() => props.modelValue,
(val) => {
if (props.isEcho && props.onlyId) selecteds.value = val
}
)
const selectHandler = (rows) => {
selecteds.value = rows
}
const close = async (done) => {
if (isArray(selecteds.value) && selecteds.value.length > 0) {
const response = await commonApi.getUserInfoByIds({ ids: selecteds.value })
if (!isEmpty(response) && isArray(response.data)) {
userList.value = response.data.map((item) => {
return `${item.username}(${item.id})`
})
if (props.onlyId) {
emit("update:modelValue", selecteds.value)
} else {
emit("update:modelValue", response.data)
}
emit("success", true)
Message.success("选择成功")
}
} else {
emit("update:modelValue", [])
userList.value = []
}
done(true)
}
const crud = ref({
showIndex: false,
api: commonApi.getUserList,
rowSelection: props.multiple ? { type: "checkbox", showCheckedAll: true } : { type: "radio" }
})
const columns = ref([
{ title: "账户", dataIndex: "username", search: true },
{ title: "昵称", dataIndex: "nickname", search: true },
{ title: "手机", dataIndex: "phone", search: true },
{ title: "邮箱", dataIndex: "email", search: true },
{
title: "部门",
dataIndex: "dept_id",
search: true,
formType: "tree-select",
hide: true,
dict: { url: "system/common/getDeptTreeList" }
},
{
title: "角色",
dataIndex: "role_id",
search: true,
formType: "select",
hide: true,
dict: { url: "system/common/getRoleList", props: { label: "name", value: "code" } }
},
{
title: "岗位",
dataIndex: "post_id",
search: true,
formType: "select",
hide: true,
dict: { url: "system/common/getPostList", props: { label: "name", value: "code" } }
}
])
</script>
<style scoped>
:deep(.arco-tabs-nav-type-capsule .arco-tabs-nav-tab) {
justify-content: flex-start;
}
</style>

View File

@@ -0,0 +1,41 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-input v-model="val" :label="props.title" disabled class="w-full" />
</template>
<script setup>
import { ref, watch } from "vue"
import { useUserStore } from "@/store"
const user = useUserStore().user
const val = ref()
const emit = defineEmits(["update:modelValue"])
const props = defineProps({
modelValue: [String, Number],
title: { type: String, default: "用户信息" },
field: { type: String, default: "id" }
})
val.value = user[props.field] ? user[props.field].toString() : user.id.toString()
watch(
() => val.value,
(vl) => emit("update:modelValue", vl),
{ immediate: true }
)
</script>
<style scoped>
:deep(.arco-select-option-content) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,105 @@
<script setup>
import { ref, onMounted } from "vue"
import { Message } from "@arco-design/web-vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const codeText = ref("")
const verfiyCanvas = ref(null)
const props = defineProps({
height: { type: Number, default: 36 },
width: { type: Number, default: 120 },
pool: { type: String, default: "abcdefghjkmnpqrstuvwxyz23456789" },
size: { type: Number, default: 4 },
showError: { type: Boolean, default: true }
})
const checkResult = (verifyCode) => {
if (!verifyCode || verifyCode.length === 0) {
props.showError && Message.error(t("sys.verifyCode.notice"))
return false
}
if (verifyCode.toLowerCase() !== codeText.value.toLowerCase()) {
props.showError && Message.error(t("sys.verifyCode.error"))
generateCode()
return false
} else {
return true
}
}
const randomNum = (min, max) => {
return parseInt(Math.random() * (max - min) + min)
}
const randomColor = (min, max) => {
const r = randomNum(min, max)
const g = randomNum(min, max)
const b = randomNum(min, max)
return `rgb(${r},${g},${b})`
}
const generateCode = () => {
codeText.value = ""
const ctx = verfiyCanvas.value.getContext("2d")
ctx.fillStyle = randomColor(230, 255)
ctx.fillRect(0, 0, props.width, props.height)
for (let i = 0; i < props.size; i++) {
let currentText = "" + props.pool[randomNum(0, props.pool.length)]
codeText.value += currentText
ctx.font = "36px Simhei"
ctx.textAlign = "center"
ctx.fillStyle = randomColor(80, 150)
ctx.fillText(currentText, (i + 1) * randomNum(20, 25), props.height / 2 + 13)
}
for (let i = 0; i < 5; i++) {
ctx.beginPath()
ctx.moveTo(randomNum(0, props.width), randomNum(0, props.height))
ctx.lineTo(randomNum(0, props.width), randomNum(0, props.height))
ctx.strokeStyle = randomColor(180, 230)
ctx.closePath()
ctx.stroke()
}
for (let i = 0; i < 40; i++) {
ctx.beginPath()
ctx.arc(randomNum(0, props.width), randomNum(0, props.height), 1, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = randomColor(150, 200)
ctx.fill()
}
ctx.restore()
ctx.save()
return codeText
}
onMounted(() => {
generateCode()
})
const refresh = () => {
generateCode()
}
defineExpose({ checkResult, refresh })
</script>
<template>
<a-tooltip :content="t('sys.verifyCode.switch')">
<canvas ref="verfiyCanvas" class="canvas" :width="props.width" :height="props.height" @click="refresh" />
</a-tooltip>
</template>
<style scoped lang="less">
:deep(.arco-input-append) {
padding: 0 !important;
}
.canvas {
cursor: pointer;
}
</style>