根据日常开发,封装表单组件,以及table组件快速构建列表及编辑页面
组件:bl-form-item
<template>
<a-row :gutter="24">
<a-col :span="24 / colNum" v-for="(item, index) in rowlist" :key="index">
<a-form-item
v-if="item.type != 'custom' && !item.hide"
:field="item.value"
:label="item.label"
:label-col-flex="isInline ? '' : labelWidth"
:rules="item.rules || null"
>
<!-- 输入框 -->
<template v-if="item.type == 'input'">
<a-input :max-length="20" v-model="filterForm[item.value]" placeholder="请输入" allow-clear />
<span class="normal" v-if="item.units"> {{ item.units }}</span>
</template>
<!-- 下拉选择 -->
<a-select v-if="item.type == 'select'" v-model="filterForm[item.value]" placeholder="请选择" allow-clear>
<a-option
v-for="option in Array.isArray(item.options) ? item.options : transformOption(item.options)"
:value="item.optionAttr ? option[item.optionAttr.value] : option.value"
:key="item.optionAttr ? option[item.optionAttr.value] : option.value"
>{{ item.optionAttr ? option[item.optionAttr.label] : option.label }}</a-option
>
</a-select>
<!-- 单选框 -->
<a-space size="large" v-if="item.type == 'radio'">
<a-radio-group v-model="filterForm[item.value]">
<a-radio
v-for="option in Array.isArray(item.options) ? item.options : transformOption(item.options)"
:value="item.optionAttr ? option[item.optionAttr.value] : option.value"
:key="item.optionAttr ? option[item.optionAttr.value] : option.value"
>
{{ item.optionAttr ? option[item.optionAttr.label] : option.label }}
</a-radio>
</a-radio-group>
</a-space>
<!-- 级联选择 -->
<a-cascader
v-if="item.type == 'cascader'"
v-model="filterForm[item.value]"
placeholder="请选择"
allow-clear
:field-names="item.fieldNames || null"
:multiple="item.multiple || false"
:options="Array.isArray(item.options) ? item.options : transformOption(item.options)"
/>
<!-- 树选择 -->
<a-tree-select
v-if="item.type == 'treeSelect'"
v-model="filterForm[item.value]"
placeholder="请选择"
:multiple="item.multiple || false"
:tree-checkable="item.treeCheckable || false"
:fieldNames="item.fieldNames || null"
:data="Array.isArray(item.options) ? item.options : transformOption(item.options)"
>
</a-tree-select>
<!-- 数字输入框 -->
<!-- :formatter="formatter" :parser="parser" -->
<template v-if="item.type == 'numberInput'">
<a-input-number
max-length="13"
v-model="filterForm[item.value]"
:placeholder="$t('numDigital.pleaseEnter')"
:min="item.min"
:max="item.max"
:precision="getPrecision(item.precision)"
/>
<span class="normal" v-if="item.units"> {{ item.units }} </span>
</template>
<!-- 多行输入框 -->
<a-textarea v-if="item.type == 'textArea'" class="textarea" placeholder="请输入" v-model="filterForm[item.value]" max-length="300" />
<!-- 日期选择 -->
<a-date-picker style="width: 100%" value-format="YYYY-MM-DD " v-if="item.type == 'datePicker'" v-model="filterForm[item.value]" />
<!-- 日期范围 -->
<!-- :value-format="item.valueFormat?item.valueFormat:'YYYY-MM-DD'" -->
<a-range-picker v-if="item.type == 'rangePicker'" @change="timeChange(item)" v-model="filterForm[item.value]" />
<!-- 月份选择 -->
<a-month-picker style="width: 100%" v-if="item.type == 'monthPicker'" v-model="filterForm[item.value]" />
<!-- 月份范围 -->
<a-range-picker mode="month" v-if="item.type == 'monthRangePicker'" v-model="filterForm[item.value]" />
<!-- 输入范围 -->
<template v-if="item.type == 'inputRange'">
<a-input max-length="20" v-model="filterForm[item.value[0]]" placeholder="请输入" />
<span class="normal"> 至 </span>
<a-input max-length="20" v-model="filterForm[item.value[1]]" placeholder="请输入" />
<span class="normal" v-if="item.units"> {{ item.units }}</span>
</template>
<!-- 输入数字范围 -->
<template v-if="item.type == 'numberInputRange'">
<!-- :formatter="formatter" :parser="parser" -->
<a-input-number
max-length="13"
v-model="filterForm[item.value[0]]"
placeholder="请输入"
:precision="getPrecision(item.precision)"
:min="item.min"
:max="item.max"
/>
<span class="normal"> 至 </span>
<a-input-number
max-length="13"
v-model="filterForm[item.value[1]]"
placeholder="请输入"
:precision="getPrecision(item.precision)"
:min="filterForm[item.value[0]]"
:max="item.max"
/>
<span class="normal" v-if="item.units"> {{ item.units }}</span>
</template>
<span v-if="item.tips" class="tips">{{ item.tips }}</span>
</a-form-item>
<slot v-if="item.custom" :name="'item-' + item.custom" v-bind="{ item }" />
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch, onBeforeMount, nextTick, reactive, getCurrentInstance } from 'vue'
import useLoading from '@/hooks/loading'
const { loading, setLoading } = useLoading()
import { valid } from 'mockjs'
const emit = defineEmits(['uploadOnsuccess'])
const props = defineProps({
isInline: {
type: Boolean,
default: true,
},
labelWidth: {
type: String,
default: '90px',
},
type: {
type: String,
default: 'search',
},
rowlist: Array,
/**
* @example
rowlist:[
{ type: 'input', label: '宠物编号/名称', value: 'keyword' },
{ type: 'cascader', label: '宠物类别/品种',value: 'sort',options: deptOptions,fieldNames: {value: 'id', label: 'deptName'}},
{ type: 'treeSelect',label: '部门',value: 'dept', options: deptOptions, fieldNames:{key: 'id',title: 'deptName',children: 'children'}},
{ type: 'select', label: '状态', value: 'status', options: [{label:'正常',value:'1'},{label:'停用',value:'2'}] },
{ type: 'select', label: '状态2', value: 'status2', optionAttr:{'label':'name','value':'code'},options: [{name:'正常',code:'1'},{name:'停用',code:'2'}] },
{ type: 'select', label: '状态3', value: 'status3', options: 'stateOptions' },
{ type: 'rangePicker', label: '时间', value: 'reportTime',startTimeField:'startTime',endTimeField:'endTime'},
] */
filterForm: Object,
colNum: {
type: Number,
default: 3,
},
})
onMounted(() => {})
const timeChange = (item: any) => {
if (props.filterForm[item.value] && props.filterForm[item.value].length > 0) {
props.filterForm[item.startTimeField || 'startTime'] = props.filterForm[item.value][0] + ' 00:00:00'
props.filterForm[item.endTimeField || 'endTime'] = props.filterForm[item.value][1] + ' 23:59:59'
} else {
props.filterForm[item.startTimeField || 'startTime'] = ''
props.filterForm[item.endTimeField || 'endTime'] = ''
}
}
const transformOption = (name) => {
return []
}
//对下拉框选项处理
let optionsData = reactive({
optionsObj: {},
})
/* watch(
() => props.filterForm,
(val) => {
transFormToNumber(props.list);
},
{ deep: true }
); */
//转换成数字
const transFormToNumber = (rowList: any) => {
for (let i = 0; i < rowList.length; i++) {
for (let j = 0; j < rowList[i].length; j++) {
const itemType = rowList[i][j].type
const itemValue = rowList[i][j].value
if (itemType == 'numberInput' && props.filterForm[itemValue]) {
const _val = props.filterForm[itemValue]
props.filterForm[itemValue] = Number(_val)
}
if (itemType == 'numberInputRange' && props.filterForm[itemValue[0]] && props.filterForm[itemValue[1]]) {
const _val1 = props.filterForm[itemValue[0]]
const _val2 = props.filterForm[itemValue[1]]
props.filterForm[itemValue[0]] = Number(_val1)
props.filterForm[itemValue[1]] = Number(_val2)
}
}
}
}
const formatter = (value: any) => {
const values = value.split('.')
values[0] = values[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return values.join('.')
}
const parser = (value: any) => {
const val = value.replace(/,/g, '')
return val
}
//数字输入精度
const getPrecision = computed(() => {
return (precisionVal: Number) => {
if (precisionVal) {
return precisionVal
} else if (precisionVal == 0) {
return precisionVal
} else {
return 2
}
}
})
const toNumber = computed(() => {
return (val: any) => {
return Number(val)
}
})
//表单校验规则
const getFormRules = computed(() => {
return (rule: any, label: any, type: any) => {
let typeMessage = ''
if (type == 'select' || type == 'radio' || type == 'datePicker' || type == 'rangePicker') {
typeMessage = '请选择'
} else {
typeMessage = '请输入'
}
if (rule && rule.required) {
let ruleArr: any = [{ required: true, message: typeMessage + label }]
return ruleArr
} else {
return []
}
}
//
})
</script>
<style lang="less" scoped>
.normal {
flex: none;
color: var(--color-text-2);
font-size: 14px;
}
.textarea {
/* width: 464px;
height: 120px; */
}
.tips {
/* position: absolute;
z-index: 99;
display: inline-block;
width: 500px; */
position: absolute;
display: block;
left: calc(100% + 5px);
z-index: 99;
width: 366px;
line-height: 32px;
text-align: left;
font-size: 12px;
color: var(--color-neutral-6);
}
:deep {
.arco-form-item-label-col > .arco-form-item-label {
overflow: hidden;
white-space: nowrap;
}
.arco-radio-group .arco-radio {
margin-right: 0;
}
.arco-row {
position: relative;
}
}
</style>
bl-filter-form组件
<template>
<div class="filter-container">
<div class="p-tit">查询</div>
<a-row class="grid-demo">
<a-col :span="20">
<bl-form-item v-bind="$attrs" :filterForm="filterForm"></bl-form-item>
</a-col>
<a-col :span="4" class="actions">
<a-space size="small">
<a-button type="primary" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>查 询</template>
</a-button>
<a-button @click="handleReset">
<template #icon>
<icon-sync />
</template>
<template #default>重 置</template>
</a-button>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import blFormItem from '@/components/blade/bl-form-item.vue';
import {
defineEmits,
ref,
computed,
onMounted,
watch,
onBeforeMount,
defineExpose,
nextTick,
reactive,
getCurrentInstance,
} from 'vue';
const props = defineProps({
filterForm: Object,
});
const emit = defineEmits(['getList']);
/******查询*****/
onMounted(() => {});
//查询
const handleSearch = () => {
emit('getList', {
filterForm: props.filterForm,
page: {
pageSize: 10,
currPage: 1,
},
});
};
//重置
const handleReset = () => {
const len = Object.keys(props.filterForm).length || 0;
if (len > 0) {
for (let key in props.filterForm) {
props.filterForm[key] = '';
}
}
emit('getList', {
filterForm: props.filterForm,
page: {
pageSize: 10,
currPage: 1,
},
});
};
</script>
<style lang="less" scoped>
.filter-container {
margin-bottom: 24px;
background: var(--color-bg-2);
border-bottom: 1px solid #f7f8fa;
.p-tit {
height: 40px;
margin-bottom: 10px;
color: var(--color-text-1);
font-size: 18px;
line-height: 40px;
}
.actions {
padding-left: 10px;
text-align: right;
}
}
</style>
bl-table组件:
<template>
<div class="table-wrapper">
<div id="scrollContent">
<a-table
ref="MyTable"
:columns="tableColumns"
:data="tabledata"
:scroll="{
x: tableColumns.length * 130 || 1200,
y: customHeight ? customHeight : tableScrollY || 200,
}"
:style="{
height: (customHeight ? customHeight : tableScrollY || 200) + 'px',
}"
:row-key="recordIdAttr"
:pagination="false"
@selection-change="handleSelectionChange"
@sorter-change="handleSorterChange"
@expanded-change="handleExpandedChange"
:row-selection="checkbox ? 'rowSelection' : undefined"
:loading="loading"
:defaultExpandAllRows="isExpandAll"
>
<!-- <a-table-column type="selection" width="55">
</a-table-column> -->
<template #columns>
<a-table-column title="序号" width="70" v-if="showIndex">
<template #cell="{ column, record, rowIndex }">
<span>{{ (pageParams.currPage - 1) * pageParams.pageSize + rowIndex + 1 }}</span>
</template>
</a-table-column>
<a-table-column
:title="col.title"
v-for="col in tableColumns"
:key="col.id"
:data-index="col.custom ? '' : col.dataIndex"
:sortable="col.sortable || ''"
:ellipsis="col.ellipsis || false"
:fixed="col.fixed || ''"
:width="col.width || 150"
:align="col.align || 'left'"
:tooltip="col.tooltip || true"
>
<template #cell="{ column, record, rowIndex }" v-if="col.custom">
<slot :name="'table-' + col.custom" v-bind="{ record, rowIndex, column }"></slot>
</template>
</a-table-column>
<a-table-column title="操作" :width="actionWidth" v-if="hasAction" fixed="right" align="center">
<template #cell="{ column, record, rowIndex }">
<slot name="table-action" v-bind="{ record, rowIndex, column }"></slot>
</template>
</a-table-column>
</template>
</a-table>
</div>
<div id="page" class="page" v-show="!cannotPagination">
<a-pagination
:total="pageParams.total"
:current="pageParams.currPage"
:page-size="pageParams.pageSize"
show-page-size
show-jumper
show-total
@change="handleChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted, watch, onBeforeMount, nextTick, reactive, getCurrentInstance } from 'vue'
import { Table } from '@arco-design/web-vue'
import { ArgumentsType } from '@vueuse/core'
const props = defineProps({
/**
* @params 是否显示序号 默认显示
*/
showIndex: {
type: Boolean,
default: true,
},
/**
* @params 是record 的唯一标志名称,默认为id,用于传递到详情页面
*/
recordIdAttr: {
type: String,
default: 'id',
},
/**
* @params 自定义table滚动区域高度
*/
customHeight: {
type: Number,
},
/**
* @params 列表中每列对应字段
*/
tableColumns: {
type: Array,
default: function () {
return []
},
},
/**
* @example
* tableColumns: [
{ title: '宠物编号', dataIndex: 'Num', fixed: 'left', width:180 },
{ title: '宠物名称', dataIndex: 'name',ellipsis:true },
{ title: '宠物名类别/名称', dataIndex: 'sort', custom: 'sort' },
{ title: '宠物性别', dataIndex: 'sex', custom: 'sex' ,align:'center'},
{ title: '宠物年龄', dataIndex: 'age' ,align:'center'},
{
title: '检测报告数',
dataIndex: 'reportNum',
sortable: {
sortDirections: ['ascend'],
},
align:'center'
},
{
title: 'AI问诊数',
dataIndex: 'AINum',
sortable: {
sortDirections: ['ascend'],
},
align:'center'
},
align:'center'
},
{
title: '线上问诊数',
dataIndex: 'onlineNum',
sortable: {
sortDirections: ['ascend'],
},
{ title: '状态', dataIndex: 'status', custom: 'status' },
],
*/
/**
* @params 列表数据
*/
tabledata: {
type: Array,
default: function () {
return []
},
},
/**
* @params 是否有操作栏 默认显示操作列
*/
hasAction: {
type: Boolean,
default: true,
},
/**
* @params 是否有复选框 默认显示操作列
*/
checkbox: {
type: Boolean,
default: true,
},
/**
* @params 复选框选择的数据列ID
*/
rowKeys: {
type: Array,
default: function () {
return []
},
},
/**
* @params 操作栏 宽度 默认显200
*/
actionWidth: {
type: Number,
default: 200,
},
/**
* @params 分页参数
*/
pageParams: {
type: Object,
default: function () {
return {
total: 0,
currPage: 1,
pageSize: 10,
}
},
},
/**
* @params 加载状态
*/
loading: {
type: Boolean,
default() {
return false
},
},
/**
* @params 展开状态
*/
isExpandAll: {
type: Boolean,
default() {
return true
},
},
/**
* 是否需要分页
*/
cannotPagination: {
type: Boolean,
default() {
return false
},
},
})
// 调用组件实例
const MyTable = ref()
const tableMethods = {}
Object.keys(Table.methods).forEach((i) => {
// debugger
tableMethods[i] = (...args) => MyTable.value[i].apply(this, args)
})
defineExpose({
...tableMethods,
})
const emit = defineEmits(['pageChange', 'selectionChange', 'sorterChange', 'expandChange'])
/******tableScrollY*****/
const tableScrollY = ref(1000)
onMounted(() => {
calcTableScrollY()
//监听屏幕尺寸变化调整滚动宽高值
window.addEventListener('resize', calcTableScrollY)
})
onUnmounted(() => {
window.removeEventListener('resize', calcTableScrollY)
})
const calcTableScrollY = () => {
tableScrollY.value = calculateHeight()
}
/**********************分页********************** */
//页码改变
const handleChange = (value) => {
props.pageParams.currPage = value
emit('pageChange', props.pageParams)
}
//数据条数改变
const handlePageSizeChange = (value) => {
props.pageParams.pageSize = value
props.pageParams.currPage = 1 // 数据条数改变,页码置为1;
emit('pageChange', props.pageParams)
}
const handleSelectionChange = (rowKeys: (string | number)[]) => {
console.log('勾选的表格数据', rowKeys)
emit('selectionChange', rowKeys)
}
/**
* 排序规则发生改变时触发
*/
const handleSorterChange = (dataIndex: string, direction: string) => {
emit('sorterChange', dataIndex, direction)
}
/**
* 已展开的数据行发生改变时触发
*/
const handleExpandedChange = (rowKeys: (string | number)[]) => {
emit('expandChange', rowKeys)
}
/******************* 计算表单高度 *********************/
//计算table实际高度
const calculateHeight = (_cutClassNames = [], cutHeight = 0) => {
let otherHeight = 0 //caculateOtherHeight(cutClassNames);
const windowHeight = document.body.clientHeight || document.documentElement.clientHeight || window.innerHeight // 窗口高度
const top = document.getElementById('scrollContent') ? document.getElementById('scrollContent').getBoundingClientRect().top : 0 //滚动区域到顶部距离
const pageHeight = document.getElementById('page').offsetHeight //分页高度
const footerHeight = document.getElementById('footer').offsetHeight //footer公司信息高度
const pageContent = document.getElementById('pageContent')
let bottomPx = window.getComputedStyle(pageContent).paddingBottom || 0
let bottomPxM = window.getComputedStyle(pageContent).marginBottom || 0
const bottom = Number(bottomPx.replace('px', '') || 0)
const bottomM = Number(bottomPxM.replace('px', '') || 0)
const total = top + bottom + bottomM + otherHeight + pageHeight + footerHeight
const height = windowHeight - total - cutHeight
return height
}
// 计算需要减去的div高度
const caculateOtherHeight = (cutClassNames = []) => {
let total = 0
if (cutClassNames && cutClassNames.length > 0) {
cutClassNames.forEach((className) => {
const h = document.getElementsByClassName(className)[0].offsetHeight || 0
total = total + h
})
}
return total
}
</script>
<style lang="less" scoped>
.page {
box-sizing: border-box;
height: 50px;
line-height: 50px;
text-align: center;
}
</style>
文件上传:
<template>
<div ref="downloadDom">
<a-upload
v-if="type == 'add' || type == 'edit'"
:action="uploadUrl"
:headers="headers"
@success="uploadSuccess"
:on-before-remove="beforeRemove"
name="files"
:image-preview="imagePreview"
@before-upload="beforeUpload"
:limit="limit"
:accept="accept"
:file-list="reactiveData.fileList"
:default-file-list="reactiveData.fileList"
:show-upload-button="showUploadBtn"
:show-remove-button="showRemoveBtn"
:download="true"
:showFileList="showFileList"
:fileSize="fileSize"
:list-type="listType"
>
<template #upload-button v-if="custom">
<slot name="button"></slot>
</template>
</a-upload>
<div v-else>
<template v-if="reactiveData.fileList && reactiveData.fileList.length > 0">
<a-link
style="display: block"
v-for="(file, index) in reactiveData.fileList"
:download="file.name.substring(file.name.lastIndexOf('/') + 1)"
:key="index"
:href="file.url"
:hoverable="false"
>
<a-space>
<icon-file />
{{ file.name.substring(file.name.lastIndexOf('/') + 1) }}
</a-space>
</a-link>
</template>
<template v-else> 暂无 </template>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, defineEmits, defineProps, defineExpose, watch, toRaw, getCurrentInstance, onMounted, nextTick } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import useLoading from '@/hooks/loading'
const { loading, setLoading } = useLoading()
import { getToken } from '@/utils/auth'
const emit = defineEmits(['onsuccess', 'beforeUpload'])
const uploadUrl = ref("");
uploadUrl.value=import.meta.env.VITE_DOWNLOAD_HOST+'/file/upload';
const props = defineProps({
/**
* @params 文件列表数据
*/
fileList: {
type: Array,
default: [],
},
/**
* @params 接收的上传文件类型
*/
accept: {
type: String,
default: '.png,.jpg,.jpeg',
},
/**
* @params 上传文件个数
*/
limit: {
type: Number,
default: 1,
},
/**
* @params 图片预览功能是否展示
*/
imagePreview: {
type: Boolean,
default: true,
},
/**
* @params 上传图标是否展示
*/
showUploadBtn: {
type: Boolean,
default: true,
},
/**
* @params 删除图标是否展示
*/
showRemoveBtn: {
type: Boolean,
default: true,
},
/**
* @params edit add新增编辑可以上传 view只是查看
*/
type: {
type: String,
default: 'add',
},
/**
* @params 上传图片大小最大多少M
*/
fileSize: {
type: Number,
default: 5,
},
/**
* @params 图片列表类型
*/
listType: {
type: String,
default: 'picture-card',
},
/**
* @params 自定义上传图标的插槽名
*/
slotName: String,
custom: {
type: Boolean,
default: false,
},
/**
* @params 是否显示文件列表
*/
showFileList: {
type: Boolean,
default: true,
},
})
const reactiveData = reactive({
fileList: [],
})
//监听
watch(
() => props.fileList,
(val) => {
reactiveData.fileList = props.fileList
nextTick(() => {
addClickEvent()
})
},
{ deep: true, immediate: true }
)
/*******upload Header********/
const headers = computed(() => {
const token = getToken()
return {
client: '1', // 客户端类型 1 web端 2 移动端
Authorization: `${token}`,
}
})
/* 删除 */
const beforeRemove = (file) => {
let index = reactiveData.fileList.findIndex((item) => item.id === file.id)
reactiveData.fileList.splice(index, 1)
emit('onsuccess', reactiveData.fileList)
}
/* 上传前文件大小限制提醒 */
const beforeUpload = (file) => {
const isLtM = file.size / 1024 / 1024 < props.fileSize
if (!isLtM) {
Message.error('文件不能超过M' + props.fileSize + 'M')
}
emit('beforeUpload')
return isLtM
}
/* 上传成功 */
const uploadSuccess = (file) => {
const res = file.response
if (res?.data) {
let data = res.data
let imgPath = data[0] ? data[0].filePath : ''
let fileId = data[0] ? data[0].fileId : ''
const obj = {
name: file.name,
id: fileId,
url: 'http://10.60.0.44:8950' + imgPath,
imgPath: imgPath,
}
reactiveData.fileList.push(obj)
let filesStr = ''
if (reactiveData.fileList && reactiveData.fileList.length) {
const imgpathArr = reactiveData.fileList.map((item) => {
return item.imgPath
})
filesStr=imgpathArr.join(',')
console.log(filesStr);
}
emit('onsuccess', reactiveData.fileList,filesStr)
}
}
const { proxy } = getCurrentInstance()
const addClickEvent = () => {
if (!proxy.$refs['downloadDom']) {
return
}
let downloadDom = proxy.$refs['downloadDom'].getElementsByTagName('a')
let links = Object.values(downloadDom)
links.forEach((link) => {
link.onclick = (e) => {
e.preventDefault()
downloadFile(link.href + '?secret=' + keyId.value, link.download)
}
})
}
//文件下载
const downloadFile = (url: any, fileName: any) => {
fetch(url).then((res) => {
res.blob().then((blob) => {
const blobUrl = window.URL.createObjectURL(blob)
// 这里的文件名根据实际情况从响应头或者url里获取
const filename = fileName
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.click()
window.URL.revokeObjectURL(blobUrl)
})
})
}
</script>
<style lang="less" scoped></style>
列表页面:
<template>
<div class="page_right">
<!-- 筛选列表 -->
<bl-filter-form :isInline="false" :rowlist="rowlist" :filterForm="filterForm" @getList="getList"></bl-filter-form>
<div class="table-box">
<!-- 列表数据 -->
<bl-table
:pageParams="table.page"
:tableColumns="table.tableColumns"
:tabledata="table.tabledata"
@pageChange="handlePageChange"
:showIndex="false"
>
<template #table-sex="{ record }">
{{ record.petGender == 1 ? '公' : '母' }}
</template>
<template #table-age="{ record }"> {{ record.ageYear }}{{ record.ageMonth }} </template>
<template #table-status="{ record }">
<template v-if="record.status"> <span class="prohibition"></span>禁用</template>
<template v-else> <span class="normal"></span>正常 </template>
</template>
<template #table-sort="{ record }"> {{ record.animalType }}/{{ record.petType }} </template>
<template #table-action="{ record }">
<a-space size="mini">
<a-button type="text" @click="gotoPage('petManagedetail', { id: record.id })">详 情</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">删 除</a-button>
</a-space>
</template>
</bl-table>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed, onMounted, onBeforeUnmount, getCurrentInstance, onUnmounted, onBeforeMount } from 'vue'
/* import { Message, Modal } from '@arco-design/web-vue'; */
import blFilterForm from '@/components/blade/bl-filter-form.vue'
import blTable from '@/components/blade/bl-table.vue'
import { petsPage, petsCatalogueTree } from '@/api/petManagement/pet'
/* import { loginLogDept } from '@/api/systemManage/logManagement'; */
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
/* 筛选 */
const filterForm = reactive({
petsKeyWord: '',
petType: '',
status: '',
})
/* 请求宠物类别品种树数据 */
const petsCatalogOptions = ref([])
const getPetsCatalogueTreeData = async () => {
let res = await petsCatalogueTree()
petsCatalogOptions.value = res.data || []
console.log(res)
}
const rowlist = reactive([
{ type: 'input', label: '宠物编号/名称', value: 'petsKeyWord' },
{
type: 'cascader',
label: '宠物类别/品种',
value: 'petType',
options: petsCatalogOptions,
fieldNames: { value: 'id', label: 'name' },
},
/* { type: 'treeSelect',label: '部门',value: 'deptID', options: deptOptions, fieldNames:{key: 'id',title: 'deptName',children: 'children'}}, */
{
type: 'select',
label: '状态',
value: 'status',
options: [
{ label: '正常', value: '1' },
{ label: '停用', value: '2' },
],
},
])
/* table列表 */
const table = reactive({
tabledata: [],
page: { total: 0, pageSize: 10, currPage: 1 },
tableColumns: [
{ title: '宠物编号', dataIndex: 'id', fixed: 'left', width: 190 },
{
title: '宠物名称',
dataIndex: 'petName',
ellipsis: true,
tooltip: true,
width: 190,
},
{ title: '宠物名类别/品种', dataIndex: 'sort', custom: 'sort' },
{
title: '宠物性别',
dataIndex: 'petGender',
custom: 'sex',
align: 'center',
},
{ title: '宠物年龄', dataIndex: 'age', align: 'center', custon: 'age' },
{
title: '检测报告数',
dataIndex: 'medicalReportNum',
sortable: {
sortDirections: ['ascend'],
},
align: 'center',
},
{
title: 'AI问诊数',
dataIndex: 'aiInterrogationNum',
sortable: {
sortDirections: ['ascend'],
},
align: 'center',
},
{
title: '线上问诊数',
dataIndex: 'onlineInterrogationNum',
sortable: {
sortDirections: ['ascend'],
},
align: 'center',
},
{ title: '状态', dataIndex: 'status', custom: 'status' },
],
})
//分页
const handlePageChange = (pageParams) => {
table.page = pageParams
getList()
}
/*********路由跳转*********************** */
const gotoPage = (pageName: String, query: Object) => {
router.push({
name: pageName,
query: query,
})
}
onBeforeMount(() => {
/* getDeptListFun(); */
getPetsCatalogueTreeData()
})
onMounted(() => {
getList()
})
/******请求列表数据******/
const getList = async (data:any=null) => {
if (data) {
table.page = data.page
}
const params = Object.assign(filterForm, table.page)
let res = await petsPage(params)
table.tabledata = (res.data && res.data.list) || []
table.page = {
total: res.data.totalCount,
pageSize: res.data.pageSize,
pageNum: res.data.currPage,
}
}
//删除
const handleDelete = (id) => {}
</script>
<style lang="less" scoped></style>
新增、编辑页:
<template>
<div class="page_right page_right_detail">
<div class="d-container">
<a-form ref="formRef" :model="formData.form" @submit="handleSaveInfo">
<div class="form-block" v-for="(bitem, index) in formData.formList" :key="index">
<div class="part-tit">{{ bitem.title }}</div>
<div v-if="bitem.custom" class="b-tips">
<div>1.可选择不开票</div>
<div>2.若选择开票,开票主体一旦确认,无法更改</div>
<div>3.若选择开专票,需为公对公打款客户</div>
</div>
<bl-form-item
:colNum="bitem.colNum"
labelWidth="120px"
:filterForm="formData.form"
:rowlist="bitem.rows"
:isInline="false"
:style="{ width: bitem.width || '100%', margin: '0 auto' }"
>
<!-- 门内照 -->
<template #item-storePhotos="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules">
<uploadFiles :limit="5" @onsuccess="storePhotosUploadOnsuccess" :fileList="storePhotosList"> </uploadFiles>
</a-form-item>
</template>
<!-- 门牌号 -->
<template #item-storeNumber="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules">
<uploadFiles @onsuccess="storeNumberUploadOnsuccess" :fileList="storeNumberPhotoList"> </uploadFiles>
</a-form-item>
</template>
<!-- 行业相关资质照片 -->
<template #item-qualificationPhotos="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules">
<uploadFiles :limit="5" @onsuccess="qualificationPhotosOnsuccess" :fileList="qualificationPhotosList"> </uploadFiles>
</a-form-item>
</template>
<!-- 营业执照 -->
<template #item-businessLicense="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules">
<uploadFiles @onsuccess="businessLicenseOnsuccess" :fileList="businessLicenseList"> </uploadFiles>
</a-form-item>
</template>
<!-- 开票主体营业执照 -->
<template #item-invoiceBusinessLicense="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules" v-if="!item.hide">
<uploadFiles @onsuccess="invoiceBusinessLicenseOnsuccess" :fileList="invoiceBusinessLicenseList"> </uploadFiles>
</a-form-item>
</template>
<!-- 专票授权文件 -->
<template #item-dedicateeInvoice="{ item }">
<a-form-item :field="item.value" :label="item.label" :rules="item.rules" v-if="!item.hide">
<uploadFiles @onsuccess="dedicateeInvoiceeOnsuccess" :fileList="dedicateeInvoiceList"> </uploadFiles>
</a-form-item>
</template>
</bl-form-item>
</div>
<!-- <div>添加产品线</div> -->
<div class="btn-actions">
<a-space size="large">
<a-button type="primary" html-type="submit">保 存</a-button>
<a-button @click="handleReset">重 置</a-button>
</a-space>
</div>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
import blFormItem from '@/components/blade/bl-form-item.vue'
import useLoading from '@/hooks/loading'
import { addStore, updateStore, getStoreInfo } from '@/api/customManagement/store'
import uploadFiles from '@/components/uploadFile/index.vue'
import { Message, Modal } from '@arco-design/web-vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const { loading, setLoading } = useLoading()
onMounted(() => {
if (route.query.id) {
getIndoDetail()
}
})
const licencesBol = ref(false)
const workTimeBol = ref(false)
const invoiceBol = ref(true)
const dedicateeBol = ref(false)
const formData = reactive({
form: {
bindingPhone: '',
companyAddress: '',
contact: '',
hotline: '',
openingHours: '1',
qualificationPhotos: [],
storeAddress: '江西省赣州市章贡区沙河工业园区',
storeLatitude: '25.830455',
storeLongitude: '114.973604',
storeName: '',
timeFrameList: [],
storePhotos: [],
storeNumberPhoto: '',
businessLicense: '',
businessLicenseName: '',
businessLicenseCode: '',
identifyType: '1',
businessModel: 1,
invoiceType: 1,
isInvoice: '1',
invoiceLatitude: '25.830455',
invoiceLongitude: '114.973604',
invoiceBusinessLicense: '',
invoiceBusinessLicenseName: '',
invoiceBusinessLicenseCode: '',
courtSummons:"",
invoiceOrganizationCode:"",
invoiceTaxEnrolCertificate:"",
},
formList: [
{
colNum: 1,
width: '700px',
title: '基本信息',
rows: [
{
type: 'input',
label: '门店名称',
value: 'storeName',
tips: '门店名称会显示在小程序前端,请谨慎填写',
rules: [{ required: true, message: '请输入门店名称' }],
},
{
type: 'input',
label: '绑定手机号',
value: 'bindingPhone',
tips: '该手机号码可用于忘记密码进行短信校验,请确保填写号码的真实性',
rules: [{ required: true, message: '请输入账号绑定手机号' }],
},
{
type: 'input',
label: '联系人',
value: 'contact',
rules: [{ required: true, message: '请输入联系人' }],
},
{
type: 'select',
label: '门店地址',
value: 'storeAddress',
options: [],
rules: [{ required: true, message: '请选择门店地址' }],
},
{
type: 'input',
label: '热线电话',
value: 'hotline',
rules: [{ required: true, message: '请输入热线电话' }],
},
{
type: 'radio',
label: '营业时间',
value: 'openingHours',
options: [
{ label: '24小时全天开放', value: '1' },
{ label: '选择时间段', value: '2' },
],
rules: [{ required: true, message: '请选择营业时间' }],
},
{
type: 'rangePicker',
label: ' ',
hide: workTimeBol,
value: 'timeFrameList',
startTimeField: 'startTime',
endTimeField: 'endTime',
},
{
type: 'custom',
label: '店内照片',
value: 'storePhotos',
custom: 'storePhotos',
rules: [{ required: true, message: '请上传店内照片' }],
},
{
type: 'custom',
label: '门牌号',
value: 'storeNumberPhoto',
custom: 'storeNumber',
rules: [{ required: true, message: '请上传吗门牌号照片' }],
},
],
},
{
colNum: 1,
width: '700px',
title: '经营信息',
rows: [
{
type: 'select',
options: [{ label: '经营模式1', value: 1 }],
label: '经营模式',
value: 'businessModel',
rules: [{ required: true, message: '请选择经营模式' }],
},
{
type: 'custom',
label: '行业相关资质照片',
value: 'qualificationPhotos',
custom: 'qualificationPhotos',
rules: [{ required: true, message: '请上传行业相关资质照片' }],
},
{
type: 'custom',
label: '营业执照',
value: 'businessLicense',
custom: 'businessLicense',
rules: [{ required: true, message: '请上传营业执照' }],
},
{
type: 'input',
label: '营业执照名称',
value: 'businessLicenseName',
rules: [{ required: true, message: '请填写营业执照名称' }],
},
{
type: 'input',
label: '营业执照编码',
value: 'businessLicenseCode',
rules: [{ required: true, message: '请填写营业执照编码' }],
},
{
type: 'radio',
label: '认证类型',
value: 'identifyType',
options: [
{ label: '三证合一', value: '1' },
{ label: '三证', value: '2' },
],
rules: [{ required: true, message: '请选择认证类型' }],
},
{
type: 'input',
hide: licencesBol,
label: '组织机构代码编码',
value: 'organizationCode',
custom: 'organizationCode',
rules: [{ required: true, message: '请填写组织机构代码编码' }],
},
{
type: 'input',
hide: licencesBol,
label: '税务登记证编码',
value: 'taxCode',
custom: 'taxCode',
rules: [{ required: true, message: '请填写税务登记证编码' }],
},
],
},
{
colNum: 1,
width: '700px',
custom: true, //自定义
title: '开票信息',
rows: [
{
type: 'radio',
label: '是否开票',
value: 'isInvoice',
options: [
{ label: '是', value: '1' },
{ label: '否', value: '0' },
],
rules: [{ required: true, message: '请选择是否开票' }],
},
{
type: 'radio',
label: '开票类型',
value: 'invoiceType',
hide: invoiceBol,
options: [
{ label: '普通发票', value: 1 },
{ label: '专用发票', value: 2 },
],
rules: [{ required: true, message: '请选择是否开票' }],
},
{
type: 'custom',
label: '开票主体营业执照',
hide: invoiceBol,
value: 'invoiceBusinessLicense',
custom: 'invoiceBusinessLicense',
rules: [{ required: true, message: '请上传开票主体营业执照' }],
},
{
type: 'input',
label: '营业执照名称',
hide: invoiceBol,
value: 'invoiceBusinessLicenseName',
rules: [{ required: true, message: '请填写营业执照名称' }],
},
{
type: 'input',
label: '营业执照编码',
hide: invoiceBol,
value: 'invoiceBusinessLicenseCode',
rules: [{ required: true, message: '请填写营业执照编码' }],
},
{
type: 'custom',
label: '专票授权文件',
hide: dedicateeBol,
value: 'courtSummons',
custom: 'dedicateeInvoice',
rules: [{ required: true, message: '请上传专票授权文件' }],
},
{ type: 'input', label: '公司名称', hide: invoiceBol, value: 'companyName', rules: [{ required: true, message: '请填写公司名称' }] },
{ type: 'input', label: '税号', hide: invoiceBol, value: 'tariffItem', rules: [{ required: true, message: '请填写税号' }] },
{
type: 'input',
label: '发票收件人',
hide: invoiceBol,
value: 'invoiceReceiver',
rules: [{ required: true, message: '请填写发票收件人' }],
},
{
type: 'input',
label: '联系电话',
hide: invoiceBol,
value: 'invoiceReceiverPhone',
rules: [{ required: true, message: '请填写联系电话' }],
},
{ type: 'input', label: '所在地址', hide: invoiceBol, value: 'companyAddress', rules: [{ required: true, message: '请选择所在地址' }] },
{
type: 'input',
hide: !invoiceBol,
label: '组织机构代码编码',
value: 'invoiceOrganizationCode',
custom: 'organizationCode',
rules: [{ required: true, message: '请填写组织机构代码编码' }],
},
{
type: 'input',
hide: !invoiceBol,
label: '税务登记证编码',
value: 'invoiceTaxEnrolCertificate',
custom: 'taxCode',
rules: [{ required: true, message: '请填写税务登记证编码' }],
},
],
},
/* {
colNum: 1,
width: '700px',
title: '交易信息',
rows: [],
}, */
],
})
watch(
() => formData.form,
(val: any) => {
/* 认证类型切换 */
if (val.identifyType == '2') {
licencesBol.value = false
} else {
licencesBol.value = true
}
/* 营业时间切换 */
if (val.openingHours == '2') {
workTimeBol.value = false
} else {
workTimeBol.value = true
}
/* 是否开票切换 */
if (val.isInvoice == '1') {
invoiceBol.value = false
} else {
invoiceBol.value = true
}
/* 开票类型 */
if (val.invoiceType == 1) {
dedicateeBol.value=true
}else{
dedicateeBol.value=false
}
},
{
immediate: true,
deep: true,
}
)
//返回
const goBack = () => {
router.go(-1)
}
/*********图片上传************* */
/* 门内店照 */
const storePhotosList = ref([])
const storePhotosUploadOnsuccess = (files, filesStr) => {
storePhotosList.value = files
formData.form.storePhotos = filesStr.split(',')
}
/* 门牌号 */
const storeNumberPhotoList = ref([])
const storeNumberUploadOnsuccess = (files, filesStr) => {
storeNumberPhotoList.value = files
formData.form.storeNumberPhoto = filesStr
}
/* 行业资质照片 */
const qualificationPhotosList = ref([])
const qualificationPhotosOnsuccess = (files, filesStr) => {
qualificationPhotosList.value = files
formData.form.qualificationPhotos = filesStr.split(',')
}
/* 营业执照 */
const businessLicenseList = ref([])
const businessLicenseOnsuccess = (files, filesStr) => {
businessLicenseList.value = files
formData.form.businessLicense = filesStr
}
/* 开票主体营业执照 */
const invoiceBusinessLicenseList = ref([])
const invoiceBusinessLicenseOnsuccess = (files, filesStr) => {
invoiceBusinessLicenseList.value = files
formData.form.invoiceBusinessLicense = filesStr
}
/* 专票授权文件 */
const dedicateeInvoiceList = ref([])
const dedicateeInvoiceeOnsuccess = (files, filesStr) => {
dedicateeInvoiceList.value = files
formData.form.courtSummons = filesStr
}
/******请求数据******/
const getIndoDetail = async () => {
let res = await getStoreInfo(route.query.id)
console.log(res.data)
formData.form = Object.assign(formData.form, res.data)
console.log(formData.form)
}
const timeChange = (item: any) => {}
/* 重置 */
const formRef = ref<any>(null)
const handleReset = () => {
formRef.value.resetFields()
storePhotosList.value = []
storeNumberPhotoList.value = []
businessLicenseList.value = []
qualificationPhotosList.value = []
invoiceBusinessLicenseList.value = []
}
/******请求数据******/
const handleSaveInfo = async ({ values, errors }) => {
if (!errors) {
if (route.query.id) {
let res = await updateStore(formData.form)
if (res.code == 0) {
Message.success('修改成功')
router.go(-1)
}
} else {
let res = await addStore(formData.form)
if (res.code == 0) {
Message.success('新增成功')
router.go(-1)
}
}
}
}
</script>
<style lang="less" scoped>
.part-tit {
font-size: 18px;
height: 2em;
line-height: 2em;
padding-left: 10px;
margin: 20px 0 30px;
border-bottom: 1px solid #e5e6eb;
}
.b-tips {
color: rgb(var(--arcoblue-4));
line-height: 1.8em;
background: rgba(42, 182, 255, 0.06);
width: 700px;
margin: 0 auto 30px;
padding: 16px;
}
.btn-actions {
text-align: center;
}
</style>