chore(frontend): 落地 ADS 治理体系并收录前端底座基线代码

This commit is contained in:
zhoulei
2026-08-19 14:39:02 +08:00
parent 775e075495
commit 1f84456421
177 changed files with 18574 additions and 0 deletions
@@ -0,0 +1,105 @@
<template>
<el-breadcrumb class="h-[50px] flex items-center">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
<span
v-if="
item.redirect === 'noredirect' || index === breadcrumbs.length - 1
"
class="text-[var(--el-disabled-text-color)]"
>{{ item.meta.title }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ item.meta.title }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { onBeforeMount, ref, watch } from "vue";
import { useRoute, RouteLocationMatched } from "vue-router";
import { compile } from "path-to-regexp";
import router from "@/framework/router";
// import { translateRouteTitleI18n } from "@/framework/utils/i18n";
const currentRoute = useRoute();
const pathCompile = (path: string) => {
const { params } = currentRoute;
const toPath = compile(path);
return toPath(params);
};
const breadcrumbs = ref([] as Array<RouteLocationMatched>);
function getBreadcrumb() {
console.log(currentRoute.matched,8888);
let matched = currentRoute.matched.filter(
(item) => item.meta && item.meta.title
);
// const first = matched[0];
// if (!isDashboard(first)) {
// matched = [
// { path: "/home", meta: { title: "首页" } } as any,
// ].concat(matched);
// }
breadcrumbs.value = matched.filter((item) => {
return item.meta && item.meta.title && item.meta.breadcrumb !== false;
});
}
function isDashboard(route: RouteLocationMatched) {
const name = route && route.name;
if (!name) {
return false;
}
return (
name.toString().trim().toLocaleLowerCase() ===
"首页"
);
}
function handleLink(item: any) {
const { redirect, path } = item;
if (redirect) {
router.push(redirect).catch((err) => {
console.warn(err);
});
return;
}
router.push(pathCompile(path)).catch((err) => {
console.warn(err);
});
}
watch(
() => currentRoute.path,
(path) => {
if (path.startsWith("/redirect/")) {
return;
}
getBreadcrumb();
}
);
onBeforeMount(() => {
getBreadcrumb();
});
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
margin-left: 8px;
font-size: 14px;
line-height: 50px;
}
// 覆盖 element-plus 的样式
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
</style>
@@ -0,0 +1,20 @@
<template>
<div class="not-container">
<img src="@/assets/img/403.png" class="not-img" alt="403" />
<div class="not-detail">
<h2>403</h2>
<h4>抱歉您无权访问该页面~🙅🙅</h4>
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
</div>
</div>
</template>
<script setup lang="ts" name="403">
// import { HOME_URL } from "@/config";
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>
@@ -0,0 +1,21 @@
<template>
<div class="not-container">
<img src="@/assets/img/404.png" class="not-img" alt="404" />
<div class="not-detail">
<h2>404</h2>
<h4>抱歉您访问的页面不存在~</h4>
<h4>有可能是您没有访问权限~</h4>
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
</div>
</div>
</template>
<script setup lang="ts" name="404">
// import { HOME_URL } from "@/config";
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>
@@ -0,0 +1,20 @@
<template>
<div class="not-container">
<img src="@/assets/img/500.png" class="not-img" alt="500" />
<div class="not-detail">
<h2>500</h2>
<h4>抱歉您的网络不见了~🤦🤦</h4>
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
</div>
</div>
</template>
<script setup lang="ts" name="500">
// import { HOME_URL } from "@/config";
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>
@@ -0,0 +1,32 @@
.not-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.not-img {
margin-right: 120px;
}
.not-detail {
display: flex;
flex-direction: column;
h2,
h4 {
padding: 0;
margin: 0;
}
h2 {
font-size: 60px;
color: var(--el-text-color-primary);
}
h4 {
margin: 30px 0 20px;
font-size: 19px;
font-weight: normal;
color: var(--el-text-color-regular);
}
.el-button {
width: 100px;
}
}
}
@@ -0,0 +1,44 @@
<template>
<!-- <div class="hambuger-wrapper" :class="{active: isActive}" @click="toggleClick">
<el-icon size="20">
<Expand />
</el-icon>
</div> -->
<div class="hambuger-wrapper" @click="toggleClick">
<img :src="foldPng" style="width: 28px;height: 28px;" :style="{transform: isActive ? '' :'rotate(180deg)'}"/>
</div>
</template>
<script setup lang="ts">
import foldPng from '@/assets/img/icon/fold.png'
defineProps({
isActive: {
required: true,
type: Boolean,
default: false,
},
});
const emit = defineEmits(["toggleClick"]);
function toggleClick() {
emit("toggleClick");
}
</script>
<style lang="scss" scoped>
.hambuger-wrapper{
width: 64px;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover{
background: rgba(0,0,0,.025);
}
}
.active{
transform: rotate(180deg);
}
</style>
@@ -0,0 +1,28 @@
<template>
<img class="menu-img" :src="imgSrc"/>
</template>
<script setup lang="ts">
import { getAssetsImages } from '@/framework/utils/index'
const props = defineProps({
path: {
type: String,
default: "/menu",
},
imgName: {
type: String,
required: true,
default: "",
}
});
const imgSrc = computed(() => getAssetsImages(props.imgName, props.path));
</script>
<style scoped>
.menu-img{
margin-left: 3px;
margin-right: 8px;
}
</style>
@@ -0,0 +1,101 @@
<template>
<el-dialog v-model="dialogVisible" title="批量添加" :destroy-on-close="true" width="580px" draggable top="1vh">
<el-form label-width="120px">
<el-form-item label="模板下载:">
<el-button type="primary" @click="downloadFile">
<el-icon color="#fff">
<Download />
</el-icon> 模板下载
</el-button>
</el-form-item>
<el-form-item label="文件上传:">
<el-upload class="upload" drag :action="uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
请按模板上传文件
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type {UploadRawFile } from 'element-plus'
let props = defineProps({
templateName: {
type: String,
default: '',
},
uploadUrl: String
})
const dialogVisible = ref(false)
// 下载模板
const downloadFile = () => {
if (props.templateName === '') {
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
}
let dom = document.createElement("a");
dom.style.display = 'none'
dom.target = '_blank'
dom.href = `./templete/${props.templateName}`
document.body.appendChild(dom);
dom.click();
document.body.removeChild(dom);
}
/**
* @description 文件上传之前判断
* @param file 上传的文件
* */
const beforeExcelUpload = (file: UploadRawFile) => {
const suffix = file.name.split('.').pop()
const isExcel = ['xls', 'xlsx'].includes(suffix);
if (!isExcel)
ElMessage({type: 'warning',message: '上传文件只能是 xls / xlsx 格式' })
// const fileSize = file.size
// if (!fileSize)
// setTimeout(() => {
// ElNotification({
// title: "温馨提示",
// message: `上传文件大小不能超过 ${parameter.value.fileSize}MB`,
// type: "warning"
// });
// }, 0);
return isExcel;
};
const emit = defineEmits(['success'])
const excelUploadSuccess = (response: any) => {
emit('success', response)
}
// 上传错误提示
const excelUploadError = () => {
ElMessage({
title: "温馨提示",
message: `批量添加失败,请您重新上传!`,
type: "error"
});
};
defineExpose({
dialogVisible
});
</script>
<style lang="scss" scoped>
.upload {
width: 80%;
}
</style>
@@ -0,0 +1,93 @@
<template>
<div class="" :class="'pagination ' + { hidden: hidden }">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:background="background"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { computed, PropType } from "vue";
import { scrollTo } from "@/framework/utils/scroll-to";
const props = defineProps({
total: {
required: true,
type: Number as PropType<number>,
default: 0,
},
page: {
type: Number,
default: 1,
},
limit: {
type: Number,
default: 20,
},
pageSizes: {
type: Array as PropType<number[]>,
default() {
return [10, 20, 30, 50];
},
},
layout: {
type: String,
default: "total, sizes, prev, pager, next, jumper",
},
background: {
type: Boolean,
default: true,
},
autoScroll: {
type: Boolean,
default: true,
},
hidden: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["pagination", "update:page", "update:limit"]);
const currentPage = useVModel(props, "page", emit);
const pageSize = useVModel(props, "limit", emit);
function handleSizeChange(val: number) {
emit("pagination", { page: currentPage, limit: val });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
function handleCurrentChange(val: number) {
currentPage.value = val;
emit("pagination", { page: val, limit: props.limit });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
</script>
<style lang="scss" scoped>
.pagination {
padding: 20px 12px;
display: flex;
flex-direction: row-reverse;
&.hidden {
display: none;
}
}
</style>
@@ -0,0 +1,93 @@
<template>
<el-select v-model="value" :placeholder="placeholder" style="width: 100%;" clearable :filterable="filterable">
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px;"
:value="item.value" />
</el-select>
</template>
<script setup lang="ts">
import {ref, computed, onMounted} from 'vue'
/**
* SearchSelect
* @author nielang
* @description 用于实时检索的下拉框
* @property {String} placeholder input的placeholder
* @property {Function} request 接口请求函数
* @property {String} requestParam 接口请求参数名称
* @property {Boolean} showOptionValue 选项中是否显示value属性
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
* @property {String} optionLabel 接口用于显示的属性
* @property {Boolean} filterable 是否可搜索
*/
const props = defineProps({
modelValue:{
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
request:{
type: Function,
require: true
},
requestParam: {
type: Object,
default: {},
},
showOptionValue: {
type: Boolean,
default: true,
},
optionValue: {
type: [String, Number],
default: "id",
},
optionLabel: {
type: [String, Number],
default: "name",
},
filterable: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
interface ListItem {
value: string
label: string
}
const options = ref<ListItem[]>([])
const initOptions = () => {
if (props.request) {
props.request(props.requestParam).then((data:any) => {
options.value = data.map((item: any) => {
return {
value: item[props.optionValue],
label: (props.showOptionValue ? `${item[props.optionValue]}` : '') + item[props.optionLabel]
}
})
})
}
}
onMounted(() => {
initOptions();
});
</script>
<style scoped>
</style>
@@ -0,0 +1,98 @@
<template>
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable
:remote-method="remoteMethod" :loading="loading" clearable >
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px"
:value="item.value" />
</el-select>
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
/**
* SearchSelect
* @author nielang
* @description 用于实时检索的下拉框
* @property {String} placeholder input的placeholder
* @property {Function} request 接口请求函数
* @property {String} requestParam 接口请求参数名称
* @property {Boolean} showOptionValue 选项中是否显示value属性
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
* @property {String} optionLabel 接口用于显示的属性
*/
const props = defineProps({
modelValue:{
type: String,
default: "",
},
placeholder: {
type: String,
default: "请输入商品编码/名称",
},
request:{
type: Function,
require: true
},
requestParam: {
type: String,
default: "",
},
showOptionValue: {
type: Boolean,
default: true,
},
optionValue: {
type: [String, Number],
default: "id",
},
optionLabel: {
type: [String, Number],
default: "name",
},
})
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const loading = ref(false);
interface ListItem {
value: string
label: string
}
const options = ref<ListItem[]>([])
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
if (props.request) {
const params:any = {}
params[props.requestParam] = query
props.request(params).then((data:any) => {
loading.value = false
options.value = data.map((item: any) => {
return {
value: item[props.optionValue],
label: (props.showOptionValue ? `${item[props.optionValue]}` : '') + item[props.optionLabel]
}
})
}).finally(() => {
loading.value = false;
});
}
} else {
options.value = []
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,28 @@
<!--主题-->
<template>
<div class="form-container ab-m-t-16 title">
<el-page-header :icon="null" >
<template #title>
<div style="display:flex;align-items: center;">
<slot name="titleBack"></slot>
<span style="font-size: 25px;font-weight: 500;"><slot name="titleName"></slot></span>
</div>
</template>
<template #content>
<span style="font-size: 15px;"><slot name="titleContent"></slot></span>
</template>
<template #extra>
<slot name="titleOperate"></slot>
</template>
</el-page-header>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
.title{
padding: 10px 0px 10px 10px;;
}
</style>
@@ -0,0 +1,77 @@
<!-- 内容分隔组件 -->
<template>
<div class="title-container" >
<div class="header" :style="{ backgroundColor: bgColor, fontSize, color, borderBottom: arrow?'1px solid #CDD0D6':'' }" @click="handleShow" >
<div class="left">
<span class="line" :style="{display: showLine? 'inline' : 'none', borderColor: lineColor}"></span> <span class="name">{{ title }}</span>
</div>
<div class="right" :style="{display: arrow? 'inline' : 'none'}">
<el-icon :style="{transform: show ? 'rotate(90deg)' : '', transition: 'transform 0.2s linear'}"><ArrowRightBold /></el-icon>
</div>
</div>
<div class="content" v-show="show" :style="{height: show ? 'auto' : '0px'}">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
</script>
<script setup lang="ts">
const props = defineProps({
title: { type: String, required: true, }, //标签名称
bgColor: { type: String, default: '#fff' },//背景颜色
color: { type: String, default: '#222' },//文字颜色
fontSize: { type: String, default: '20px' },//字体大小
showLine: { type: Boolean, default: true },//是否显示图标
lineColor: { type: String, default: '' },//图标颜色
arrow: { type: Boolean, default: false },//展开、收起功能
})
const show = ref(true)
const handleShow = () => {
if (!props.arrow) {
return
}
show.value = !show.value
}
</script>
<style lang="scss" scoped>
.header {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
// border-bottom: 1px solid #CDD0D6;
.left{
.line {
width: 2px;
border: 2px solid var(--el-color-primary);
border-radius: 6px;
margin-right: 6px;
}
.name {
font-weight: bold;
}
}
.right{
margin-right: 5px;
}
}
.content{
height: 0px;
transition: height 2s ease;
}
</style>
@@ -0,0 +1,23 @@
<!-- 点击复制 -->
<template >
<el-tooltip v-if="content" content="复制" placement="top" effect="dark">
<el-icon class="ab-cursor-pointer" @click="useCopy(content)"><CopyDocument /></el-icon>
</el-tooltip>
{{ content }}
</template>
<script lang="ts">
</script>
<script setup lang="ts">
import { useCopy } from '@/framework/hooks/useCopy'
const props = defineProps({
content: { type: String, required: true,}, //内容
})
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,75 @@
<!--文档预览组件-->
<template>
<el-dialog title="预览" :model-value="dialogVisible" :before-close="handleClose" :modal="false" :show-close="true" :close-on-click-modal="false" append-to-body>
<template #title>
<span class="dialog-title">
<el-button type="primary" @click="downloadFile()">下载文件</el-button>
</span>
</template>
<div v-if="['doc', 'docx'].includes(fileType)">
<vue-office-docx :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
</div>
<div v-else-if="fileType == 'xlsx'">
<vue-office-excel :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
</div>
<div v-else-if="fileType == 'pdf'">
<vue-office-pdf :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
</div>
<div v-else-if="['png', 'jpg', 'jpeg','gif'].includes(fileType)">
<el-image :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
</div>
<div v-else="">
<el-input v-model="textarea1" style="height: 70vh;overflow: auto" autosize type="textarea" :options="{width: '100%',overflow: 'auto'}" />
</div>
</el-dialog>
</template>
<script setup lang="ts">
// 引入VueOffice组件
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import VueOfficePdf from '@vue-office/pdf'
import XEUtils from 'xe-utils'
// 引入相关样式
import '@vue-office/docx/lib/index.css'
import '@vue-office/excel/lib/index.css'
const textarea1=ref('')
const emit = defineEmits(["closePreviewDialog"]);
const props = defineProps({
fileName: { type: String, default: '' },// 文件url
fileType: { type: String, default: '' },// 文件url
fileUrl: { type: String, default: '' },// 文件url
dialogVisible: { type: Boolean, default: false },// 控制显示
})
const downloadFile = () => {
if(XEUtils.startsWith(props.fileUrl, './')||XEUtils.startsWith(props.fileUrl, '/')){
window.location.href=props.fileUrl
}else{
window.location.href=props.fileUrl
}
}
const rendered = () => {
console.log('渲染完成')
}
const errorHandler = (e:any) => {
console.error('渲染失败',e)
ElMessage({message: '文档渲染失败',type: 'error',})
}
const handleClose = (done) => {
emit('closePreviewDialog', false)
}
</script>
<script lang="ts">
</script>
@@ -0,0 +1,103 @@
<!-- 动态表单组件 -->
<!-- 新功能禁止使用该组件 -->
<template>
<div class="form-container">
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
<el-row :gutter="props.gutter">
<template v-for="(value, key, index) in props.formModel">
<el-col class="ab-p-b-5" v-if="'section'===fieldConfigMap.get(key)?.type" :span="24" v-show="isShow(fieldConfigMap.get(key))">
<AbSection :title="fieldConfigMap.get(key).name"></AbSection>
</el-col>
<el-col v-else :span="fieldConfigMap.get(key)?.span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))">
<template v-if="fieldConfigMap.get(key)?.slot">
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
<slot :name="fieldConfigMap.get(key)?.slot"></slot>
</el-form-item>
</template>
<template v-else>
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
</template>
</el-col>
</template>
<template #title>
</template>
<el-col :span="24" class="ab-text-right ab-m-b-18">
<slot v-if="$slots['button']" name="button"></slot>
<template v-else>
<el-button type="primary" @click="executeSave(formRef)">保存</el-button>
<el-button type="default" @click="formReset()">重置</el-button>
</template>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup lang="ts">
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
import AbSection from "@/framework/components/standard/AbSection/index.vue";
const formRef = ref<FormInstance>() //表单引用
const dFormRules = reactive<FormRules>({}) //验证规则
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
//组件传参
let props = defineProps({
formModel: { type: Object },//表单绑定数据
fieldConfigs: { type: Array },//表单明细配置
gutter: { type: Number, default: 0 },//栅格间隔
span: { type: Number, default: 6 },//栅格占据的列数
saveEvent: { type: Function },//保存事件
resetEvent: { type: Function },//重置事件
})
// 外部使用响应式函数,不需要监听
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
// console.log(newValue);
// },{ deep: true }
// )
/**是否显示 */
const isShow = (fieldConfig) => {
if(fieldConfig&&undefined!==fieldConfig.show&&!fieldConfig.show)
return false
return true
}
/**数据处理 */
const dataHandle = (fieldConfigs) => {
for (let fc of fieldConfigs) {
//转换map,方便获取数据
fieldConfigMap.value.set(fc.key, fc)
//设置表单验证规则
if (undefined !== fc.rules) {
dFormRules[fc.key] = fc.rules
}
//绑定表单默认数据
if(!props.formModel[fc.key])
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
}
}
dataHandle(props.fieldConfigs)
/**执行查询 */
const executeSave = async (formEl: FormInstance | undefined) => {
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
return
}
if (props.saveEvent) props.saveEvent()
}
/**重置查询条件 */
const formReset = () => {
if (props.resetEvent) {
props.resetEvent()
return
}
for (let fc of props.fieldConfigs) {
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,86 @@
<!-- 动态表单明细组件 -->
<!-- 新功能禁止使用该组件 -->
<template>
<el-form-item :label-width="props.fieldSet?.labelWidth" :label="props.fieldSet?.name" :prop="props.fieldSet?.key" :required="props.fieldSet?.required">
<!-- 文本 -->
<el-text v-if="props.fieldSet?.type==='text'" style="width: 100%;" v-model="formModel[props.fieldSet?.key]" >
<Text :data="formModel[props.fieldSet?.key]"></Text>
</el-text>
<!-- 文本框 -->
<el-input v-else-if="props.fieldSet?.type==='input'" v-model="formModel[props.fieldSet?.key]"
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请输入'+props.fieldSet?.name"
:disabled="props.fieldSet?.disabled" />
<!-- 下拉框 -->
<el-select v-else-if="props.fieldSet?.type==='select'" v-model="formModel[props.fieldSet?.key]"
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请选择'+props.fieldSet?.name"
:disabled="props.fieldSet?.disabled" clearable filterable style="width: 100%;">
<el-option v-for="item in props.fieldSet?.listData" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
</el-select>
<!-- 单选 -->
<el-radio-group v-else-if="props.fieldSet?.type==='radio'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
<el-radio v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-radio>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-else-if="props.fieldSet?.type==='checkbox'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
<el-checkbox v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-checkbox>
</el-checkbox-group>
<!-- 时间选择器 -->
<el-date-picker style="width: 100%;" v-else-if="dataPickerType.find((o: any) => o.type === props.fieldSet?.type) " v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled"
:type="props.fieldSet?.type" unlink-panels range-separator="-"
:start-placeholder="props.fieldSet?.placeholder?'开始'+props.fieldSet?.placeholder:'开始'+props.fieldSet?.name"
:end-placeholder="props.fieldSet?.placeholder?'结束'+props.fieldSet?.placeholder:'结束'+props.fieldSet?.name"
:value-format="dataPickerType.find((o: any) => o.type === props.fieldSet?.type).valueFormat" />
<!-- 不指定类型、无类型不输出 -->
<template v-else></template>
</el-form-item>
</template>
<script setup lang="ts">
import Text from "@/framework/components/standard/Text/index.vue";
/**时间选择器配置 */
let dataPickerType=[
{type:'year',valueFormat:'YYYY'},
{type:'month',valueFormat:'YYYY-MM'},
{type:'date' ,valueFormat:'YYYY-MM-DD'},
{type:'dates' ,valueFormat:'YYYY-MM-DD'},
{type:'datetime' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
{type:'week' },
{type:'datetimerange' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
{type:'daterange' ,valueFormat:'YYYY-MM-DD'},
{type:'monthrange',valueFormat:'YYYY-MM'},
]
let props = defineProps({
formModel: {type:Object},//表单函数
editable:{type:Boolean,default:true},//可编辑
//当前字段设置
fieldSet: {
type:Object,
default:{
key:String,//字段Key
name:String, //显示名称
placeholder:String, //默认提示
required:Boolean,//是否必填
labelWidth:{type:String,default:'82px'},//标签宽度
show: { type: Boolean, default: true },//是否显示
disabled:Boolean,//是否禁用
type:String,//输入类型,当前支持input、select、radio、checkbox
//列表数据,当type为select、radio、checkbox时使用
listData:{
type:Array,
default:[
{
value:String,//值
text:String,//文本显示、
disabled:Boolean,//是否禁用
}
]},
}
},
})
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,104 @@
<!-- 动态查询表单组件 -->
<!-- 新功能禁止使用该组件 -->
<template>
<div class="form-container">
<div class="more-filter" @click="criteriaShow = !criteriaShow" v-if="criteriaShowNumber < fieldConfigMap.size">
{{ criteriaShow ? '收起' : '展开' }}更多筛选
<el-icon v-if="criteriaShow"><ArrowDown /></el-icon>
<el-icon v-else><ArrowUp /></el-icon>
</div>
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
<el-row :gutter="props.gutter">
<template v-for="(value, key, index) in props.formModel">
<el-col :span="fieldConfigMap.get(key).span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))&&(!(props.criteriaShowNumber-1<index)?true:criteriaShow)">
<!-- 自定义插槽-->
<template v-if="fieldConfigMap.get(key).slot">
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
<slot :name="fieldConfigMap.get(key).slot"></slot>
</el-form-item>
</template>
<!-- 固定类型-->
<template v-else>
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
</template>
</el-col>
</template>
<el-col :span="3" class="ab-text-right ab-m-b-18" :offset="criteriaShow ? props.queryCriteriaOffset[1] : props.queryCriteriaOffset[0]">
<el-button type="primary" v-throttle:click.delay[1000]="()=>executeQuery(formRef)" >查询</el-button>
<el-button type="default" @click="formReset()">重置</el-button>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup lang="ts">
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
const criteriaShow = ref(false) //查询条件隐藏功能
const formRef = ref<FormInstance>() //表单引用
const dFormRules = reactive<FormRules>({}) //验证规则
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
//组件传参
let props = defineProps({
formModel: { type: Object },//表单绑定数据
fieldConfigs: { type: Array },//表单明细配置
gutter: { type: Number, default: 0 },//栅格间隔
span: { type: Number, default: 6 },//栅格占据的列数
queryEvent: { type: Function },//查询事件
resetEvent: { type: Function },//重置事件
queryCriteriaOffset: { type: Array, default: [0,6] },//查询条件偏移量,[收起偏移量,展开偏移量]
criteriaShowNumber: { type: Number, default: 6 },//查询条件默认显示数量
})
// 外部使用响应式函数,不需要监听
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
// console.log(newValue);
// },{ deep: true }
// )
/**是否显示 */
const isShow = (fieldConfig) => {
if(undefined!==fieldConfig.show&&!fieldConfig.show)
return false
return true
}
/**数据处理 */
const dataHandle = (fieldConfigs) => {
for (let fc of fieldConfigs) {
//转换map,方便获取数据
fieldConfigMap.value.set(fc.key, fc)
//设置表单验证规则
if (undefined !== fc.rules) {
dFormRules[fc.key] = fc.rules
}
//绑定表单默认数据
if(!props.formModel[fc.key])
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
}
}
dataHandle(props.fieldConfigs)
/**执行查询 */
const executeQuery = async (formEl: FormInstance | undefined) => {
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
return
}
if (props.queryEvent) props.queryEvent()
}
/**重置查询条件 */
const formReset = () => {
if (props.resetEvent) {
props.resetEvent()
return
}
for (let fc of props.fieldConfigs) {
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,152 @@
<!-- Excel上传组件 -->
<template>
<el-dialog v-model="dialogVisible" title="文件上传" :destroy-on-close="true" width="580px" draggable top="1vh">
<el-row justify="end">
<el-col :span="24" style="text-align: center;">
<el-text size="large">注意事项上传文件必须符合模板格式要求点此<el-link type="primary" @click="downloadFile"><el-text type="primary" size="large">下载模板</el-text></el-link > </el-text>
</el-col>
</el-row>
<el-upload ref="fileUpload" :headers="ht" :data="props.data" drag :auto-upload="false" :show-file-list="true" :action="props.uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
</el-upload>
<el-row v-show="''!==uploadResult" style="margin-top:10px" justify="end">
<el-col :span="24" style="text-align: right;">
<el-alert type="success" :closable="false">
<template #default>
<el-text >
上传结果:本次一共导入数据[<el-text type="primary">{{ uploadResult.totalNum }}</el-text>],
成功[<el-text type="success" >{{ uploadResult.successNum }}</el-text>],
<el-text v-if="uploadResult.updateNum>0" >修改[<el-text type="success" >{{ uploadResult.updateNum }}</el-text>],</el-text>
失败[<el-text type="warning" >{{ uploadResult.failureNum }}</el-text>],
<el-link type="primary" v-if="uploadResult.filePath!==''" :href="uploadResult.filePath" target="_blank">下载错误信息</el-link>
</el-text>
</template>
</el-alert>
</el-col>
</el-row>
<el-row style="margin-top:10px" justify="end">
<el-col :span="12" style="text-align: right;">
<el-button type="primary" @click="submitFile">上传</el-button>
</el-col>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import type {UploadRawFile } from 'element-plus'
import { getAllHeader } from "@/framework/store/modules/user";
const fileUpload=ref()
const ht=getAllHeader()
let props = defineProps({
//上传时附带的额外参数
data: {},
//模板名称(模板需要放在public/templete
templateName: { type: String, default: '', },
//文件上传地址
uploadUrl: String,
//是否自定义结果处理
customResultHandle:{ type: Boolean, default: false },
//上传文件最大值,单位(MB)
uploadMaxSize: { type: Number, default: 0 },
})
const dialogVisible = ref(false)
// 下载模板
const downloadFile = () => {
if (props.templateName === '') {
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
}
let dom = document.createElement("a");
dom.style.display = 'none'
dom.target = '_blank'
dom.href = `./templete/${props.templateName}`
document.body.appendChild(dom);
dom.click();
document.body.removeChild(dom);
}
const submitFile = () => {
fileUpload.value.submit()
}
/**
* @description 文件上传之前判断
* @param file 上传的文件
* */
const beforeExcelUpload = (file: UploadRawFile) => {
uploadResult.value=''
if(!['xls', 'xlsx'].includes(file.name.split('.').pop())){
ElMessage.warning('上传文件只能是 xls / xlsx 格式' )
return false;
}
if(props.uploadMaxSize>0&&file.size>(props.uploadMaxSize*1024*1024)){
ElMessage.warning('上传文件大小不能超过'+props.uploadMaxSize+'MB')
return false;
}
return true;
};
const uploadResult = ref('')
const emit = defineEmits(['updateCheck','resultHandleEvent'])
const excelUploadSuccess = (response: any) => {
const { message,status,data } = response;
if(200===response.status){
if(props.customResultHandle){
dialogVisible.value=false
emit('resultHandleEvent', data)
}else{
if(data.handleSuccess){
uploadResult.value=data
}else{
ElMessage.error("Excel文件处理失败:"+data.message);
}
}
}else{
if (status === 401) {
ElMessage.error('未通过身份验证');
}
const temp = String(status)[0]
if (temp === '9') {
ElMessage.error(message || '系统出错');
console.error('系统异常', response.data);
return
}else{
ElMessage.error('系统异常');
console.error('系统异常', response.data);
return
}
}
}
// 上传错误提示
const excelUploadError = (response: any) => {
console.error(response);
if (response.status === 401) {
ElMessage.error('未通过身份验证');
}else{
ElMessage.error('文件上传服务异常');
}
};
defineExpose({
dialogVisible
});
onMounted(() => {
uploadResult.value=''
});
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,60 @@
<!-- 底部浮动-->
<template >
<div ref="bfRootRef" :key="isFloat? 'bottom-float':'bottom-fix'" :class="[isFloat ? 'bottom-float':'bottom-fix']" >
<slot v-if="$slots['content']" name="content"></slot>
</div>
</template>
<script lang="ts">
</script>
<script setup lang="ts">
const bfRootRef= ref()
//组件传参
let props = defineProps({
height: { type: String,default:'50px' },//浮动层高度
backgroundColor: { type: String,default:'#fff' },//浮动层背景色
getWidthEvent: { type: Function },//获取浮动层宽度
})
const isFloat=ref(false)
const width=ref('')
const initScrollHeight=ref(0)
const initClientHeight=ref(0)
const handleScroll = () => {
let clientHeight=document.documentElement.clientHeight || window.innerHeight
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let temp=(scrollTop + clientHeight+bfRootRef.value.offsetHeight)
isFloat.value=(temp >= initScrollHeight.value)?false:true
}
const windowChangesListener = () => {
width.value=props.getWidthEvent()+'px'
}
onMounted(() => {
width.value=props.getWidthEvent()+'px'
initScrollHeight.value=document.documentElement.scrollHeight || document.body.scrollHeight
initClientHeight.value=document.documentElement.clientHeight || window.innerHeight
handleScroll()
window.addEventListener('scroll', handleScroll)
window.addEventListener('resize', windowChangesListener)
})
</script>
<style lang="scss" scoped>
.bottom-float{
position: fixed;
height: v-bind('props.height');
width: v-bind('width');
bottom: 0vh;
background-color: v-bind('props.backgroundColor');
z-index: 5;
}
.bottom-fix{
height: v-bind('props.height');
width: v-bind('width');
background-color: v-bind('props.backgroundColor');
}
</style>
@@ -0,0 +1,52 @@
<!--日志显示组件-->
<template>
<el-row>
<el-col :span="12"><el-text tag="b" size="large">变更前内容</el-text></el-col>
<el-col :span="12"><el-text tag="b" size="large">变更后内容</el-text></el-col>
</el-row>
<el-row style="height: 90%;margin-top: 10px;">
<el-col :span="12">
<div class="logContent">
<!-- 优先插槽 -->
<slot v-if="$slots['beforeContent']" name="beforeContent"></slot>
<!-- 优先处理JSON格式 -->
<pre v-else-if="isJsonString(props.beforeContent)">{{ JSON.parse(props.beforeContent)}}</pre>
<!-- 默认输出 -->
<el-text v-else="props.beforeContent">{{ props.beforeContent }}</el-text>
</div>
</el-col>
<el-col :span="12">
<div class="logContent">
<!-- 优先插槽 -->
<slot v-if="$slots['content']" name="content"></slot>
<!-- 优先处理JSON格式 -->
<pre v-else-if="isJsonString(props.content)">{{ JSON.parse(props.content)}}</pre>
<!-- 默认输出 -->
<el-text v-else="props.content">{{ props.content }}</el-text>
</div>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { isJsonString } from '@/framework/utils/standard/dataHandleUtil.ts'
let props = defineProps({
//变更前内容
beforeContent: { type: String, default: '', },
//变更后内容
content: { type: String, default: '', },
})
</script>
<style lang="scss" scoped>
.logContent{
border:1px solid;
border-color: #707175;
border-radius:5px;
height: 100%;
width: 90%;
padding-top: 5px;
padding-left: 10px;
}
</style>
@@ -0,0 +1,102 @@
<!-- 实时搜索下拉框组件 -->
<template>
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable :remote-method="remoteMethod" :loading="loading" clearable
:multiple="multiple" :collapse-tags="multiple" :collapse-tags-tooltip="multiple">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
import { handleSelectData } from '@/framework/utils/standard/dataHandleUtil.ts'
/**
* 实时检索组件
* SearchSelect
* @author nielang
* @description 用于实时检索的下拉框
* @property {String} modelValue 使用v-model
* @property {String} placeholder input的placeholder
* @property {Boolean} multiple 是否支持多选
* @property {Function} request 接口请求函数,默认请求参数{'keyword':'关键字'}
* @property {Function} customRequestHandle 自定义请求参数处理
* @property {Function} customResponseHandle 自定义响应结果处理
* @property {Boolean} showOptionValue 选项中是否显示value属性,使用customResponseHandle后,该参数失效
*/
const props = defineProps({
modelValue:{
type: String,
default: "",
},
placeholder: {
type: String,
default: "请输入关键字",
},
multiple: {
type: Boolean,
default: false,
},
request:{
type: Function,
require: true
},
customRequestHandle: {
type: Function,
require: false
},
customResponseHandle: {
type: Function,
require: false
},
showOptionValue: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const loading = ref(false);
interface ListItem {
value: string
label: string
}
const options = ref<ListItem[]>([])
const remoteMethod = (keyword: string) => {
if (keyword) {
loading.value = true
if (props.request) {
let params:any = {keyword:keyword}
//自定义请求参数处理
if(props.customRequestHandle)
params=props.customRequestHandle(params)
props.request(params).then((data: any) => {
//自定义响应结果处理
if(props.customRequestHandle){
options.value=props.customResponseHandle(data)
}else{
options.value=handleSelectData(data,props.showOptionValue)
}
})
}
loading.value = false
} else {
options.value = []
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,36 @@
<!--脱敏文本展示组件-->
<template>
<span :class="props.underline?'underline':''">
<slot v-if="$slots.default"></slot>
<template v-else-if="props.data" >
<el-tooltip :content="props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data" placement="bottom" effect="light">
{{ desensitize(props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data,props.type,props.custEvent) }}
</el-tooltip>
</template>
<template v-else>&nbsp;</template>
</span>
</template>
<script setup lang="ts">
import { decrypt,desensitize,DesensitizationType } from '@/framework/utils/standard/security.ts'
let props = defineProps({
data: { type: String, default: '', },//展示数据
aesDecrypt:{type:Boolean,default: true},//是否aes解密
decryptKey:{type:String,default: ''},//自定义密钥
underline:{type:Boolean,default: false},//是否需要下划线
type:DesensitizationType,//脱敏类型
custEvent:{type: Function},//自定义脱敏方法
})
</script>
<style lang="scss" scoped>
.underline {
border-bottom: 1px solid #CDD0D6;
width: 100%;
display: block;
}
</style>
@@ -0,0 +1,17 @@
<!--文本展示组件-->
<template>
<span style="border-bottom: 1px solid #CDD0D6;width: 100%;display: block;">
<slot v-if="$slots.default"></slot>
<template v-else-if="props.data" >{{ props.data }}</template>
<template v-else>&nbsp;</template>
</span>
</template>
<script setup lang="ts">
let props = defineProps({
data: { type: String, default: '', },
})
</script>
<style lang="scss" scoped>
</style>