mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-07-12 09:55:06 +08:00
Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
This commit is contained in:
@ -27,6 +27,11 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
type: String,
|
||||
default: 'GET'
|
||||
},
|
||||
// 选项解析函数
|
||||
parseFunc: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 请求参数
|
||||
data: {
|
||||
type: String,
|
||||
@ -41,35 +46,121 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否远程搜索
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 远程搜索时携带的参数
|
||||
remoteField: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const attrs = useAttrs()
|
||||
const options = ref<any[]>([]) // 下拉数据
|
||||
const loading = ref(false) // 是否正在从远程获取数据
|
||||
const queryParam = ref<any>() // 当前输入的值
|
||||
const getOptions = async () => {
|
||||
options.value = []
|
||||
// 接口选择器
|
||||
if (isEmpty(props.url)) {
|
||||
return
|
||||
}
|
||||
let data = []
|
||||
switch (props.method) {
|
||||
case 'GET':
|
||||
data = await request.get({ url: props.url })
|
||||
let url: string = props.url
|
||||
if (props.remote) {
|
||||
url = `${url}?${props.remoteField}=${queryParam.value}`
|
||||
}
|
||||
parseOptions(await request.get({ url: url }))
|
||||
break
|
||||
case 'POST':
|
||||
data = await request.post({ url: props.url, data: jsonParse(props.data) })
|
||||
const data: any = jsonParse(props.data)
|
||||
if (props.remote) {
|
||||
data[props.remoteField] = queryParam.value
|
||||
}
|
||||
parseOptions(await request.post({ url: props.url, data: data }))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptions(data: any) {
|
||||
// 情况一:如果有自定义解析函数优先使用自定义解析
|
||||
if (!isEmpty(props.parseFunc)) {
|
||||
options.value = parseFunc()?.(data)
|
||||
return
|
||||
}
|
||||
// 情况二:返回的直接是一个列表
|
||||
if (Array.isArray(data)) {
|
||||
parseOptions0(data)
|
||||
return
|
||||
}
|
||||
// 情况二:返回的是分页数据,尝试读取 list
|
||||
data = data.list
|
||||
if (!!data && Array.isArray(data)) {
|
||||
parseOptions0(data)
|
||||
return
|
||||
}
|
||||
// 情况三:不是 yudao-vue-pro 标准返回
|
||||
console.warn(
|
||||
`接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理`
|
||||
)
|
||||
}
|
||||
|
||||
function parseOptions0(data: any[]) {
|
||||
if (Array.isArray(data)) {
|
||||
options.value = data.map((item: any) => ({
|
||||
label: item[props.labelField],
|
||||
value: item[props.valueField]
|
||||
label: parseExpression(item, props.labelField),
|
||||
value: parseExpression(item, props.valueField)
|
||||
}))
|
||||
return
|
||||
}
|
||||
console.error(`接口[${props.url}] 返回结果不是一个数组`)
|
||||
console.warn(`接口[${props.url}] 返回结果不是一个数组`)
|
||||
}
|
||||
|
||||
function parseFunc() {
|
||||
let parse: any = null
|
||||
if (!!props.parseFunc) {
|
||||
// 解析字符串函数
|
||||
parse = new Function(`return ${props.parseFunc}`)()
|
||||
}
|
||||
return parse
|
||||
}
|
||||
|
||||
function parseExpression(data: any, template: string) {
|
||||
// 检测是否使用了表达式
|
||||
if (template.indexOf('${') === -1) {
|
||||
return data[template]
|
||||
}
|
||||
// 正则表达式匹配模板字符串中的 ${...}
|
||||
const pattern = /\$\{([^}]*)}/g
|
||||
// 使用replace函数配合正则表达式和回调函数来进行替换
|
||||
return template.replace(pattern, (_, expr) => {
|
||||
// expr 是匹配到的 ${} 内的表达式(这里是属性名),从 data 中获取对应的值
|
||||
const result = data[expr.trim()] // 去除前后空白,以防用户输入带空格的属性名
|
||||
if (!result) {
|
||||
console.warn(
|
||||
`接口选择器选项模版[${template}][${expr.trim()}] 解析值失败结果为[${result}], 请检查属性名称是否存在于接口返回值中,存在则忽略此条!!!`
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const remoteMethod = async (query: any) => {
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
queryParam.value = query
|
||||
await getOptions()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -80,15 +171,29 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
if (props.multiple) {
|
||||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
<el-select class="w-1/1" {...attrs} multiple>
|
||||
<el-select
|
||||
class="w-1/1"
|
||||
multiple
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
remote={props.remote}
|
||||
{...(props.remote && { remoteMethod: remoteMethod })}
|
||||
>
|
||||
{options.value.map((item, index) => (
|
||||
<el-option key={index} label={item.label} value={item.value} />
|
||||
))}
|
||||
</el-select>
|
||||
)
|
||||
}
|
||||
debugger
|
||||
return (
|
||||
<el-select class="w-1/1" {...attrs}>
|
||||
<el-select
|
||||
class="w-1/1"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
remote={props.remote}
|
||||
{...(props.remote && { remoteMethod: remoteMethod })}
|
||||
>
|
||||
{options.value.map((item, index) => (
|
||||
<el-option key={index} label={item.label} value={item.value} />
|
||||
))}
|
||||
|
@ -13,12 +13,30 @@ const selectRule = [
|
||||
control: [
|
||||
{
|
||||
value: 'select',
|
||||
condition: '=',
|
||||
condition: '==',
|
||||
method: 'hidden',
|
||||
rule: ['multiple']
|
||||
rule: [
|
||||
'multiple',
|
||||
'clearable',
|
||||
'collapseTags',
|
||||
'multipleLimit',
|
||||
'allowCreate',
|
||||
'filterable',
|
||||
'noMatchText',
|
||||
'remote',
|
||||
'remoteMethod',
|
||||
'reserveKeyword',
|
||||
'defaultFirstOption',
|
||||
'automaticDropdown'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'filterable',
|
||||
title: '是否可搜索'
|
||||
},
|
||||
{ type: 'switch', field: 'multiple', title: '是否多选' },
|
||||
{
|
||||
type: 'switch',
|
||||
@ -43,27 +61,12 @@ const selectRule = [
|
||||
title: 'autocomplete 属性'
|
||||
},
|
||||
{ type: 'input', field: 'placeholder', title: '占位符' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'filterable',
|
||||
title: '是否可搜索'
|
||||
},
|
||||
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
|
||||
{
|
||||
type: 'input',
|
||||
field: 'noMatchText',
|
||||
title: '搜索条件无匹配时显示的文字'
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'remote',
|
||||
title: '其中的选项是否从服务器远程加载'
|
||||
},
|
||||
{
|
||||
type: 'Struct',
|
||||
field: 'remoteMethod',
|
||||
title: '自定义远程搜索方法'
|
||||
},
|
||||
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
|
||||
{
|
||||
type: 'switch',
|
||||
@ -130,6 +133,7 @@ const apiSelectRule = [
|
||||
type: 'input',
|
||||
field: 'labelField',
|
||||
title: 'label 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'nickname'
|
||||
}
|
||||
@ -138,9 +142,39 @@ const apiSelectRule = [
|
||||
type: 'input',
|
||||
field: 'valueField',
|
||||
title: 'value 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'id'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'parseFunc',
|
||||
title: '选项解析函数',
|
||||
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
||||
(data: any)=>{ label: string; value: any }[]`,
|
||||
props: {
|
||||
autosize: true,
|
||||
rows: { minRows: 2, maxRows: 6 },
|
||||
type: 'textarea',
|
||||
placeholder: `
|
||||
function (data) {
|
||||
console.log(data)
|
||||
return data.list.map(item=> ({label: item.nickname,value: item.id}))
|
||||
}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'remote',
|
||||
info: '是否可搜索',
|
||||
title: '其中的选项是否从服务器远程加载'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'remoteField',
|
||||
title: '请求参数',
|
||||
info: '远程请求时请求携带的参数名称,如:name'
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { generateUUID } from '@/utils'
|
||||
import * as DictDataApi from '@/api/system/dict/dict.type'
|
||||
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule
|
||||
@ -9,6 +10,7 @@ import { selectRule } from '@/components/FormCreate/src/config/selectRule'
|
||||
export const useDictSelectRule = () => {
|
||||
const label = '字典选择器'
|
||||
const name = 'DictSelect'
|
||||
const rules = cloneDeep(selectRule)
|
||||
const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
|
||||
onMounted(async () => {
|
||||
const data = await DictDataApi.getSimpleDictTypeList()
|
||||
@ -55,7 +57,7 @@ export const useDictSelectRule = () => {
|
||||
{ label: '布尔值', value: 'bool' }
|
||||
]
|
||||
},
|
||||
...selectRule
|
||||
...rules
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { generateUUID } from '@/utils'
|
||||
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
|
||||
import { SelectRuleOption } from '@/components/FormCreate/src/type'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 通用选择器规则 hook
|
||||
@ -11,6 +12,7 @@ import { SelectRuleOption } from '@/components/FormCreate/src/type'
|
||||
export const useSelectRule = (option: SelectRuleOption) => {
|
||||
const label = option.label
|
||||
const name = option.name
|
||||
const rules = cloneDeep(selectRule)
|
||||
return {
|
||||
icon: option.icon,
|
||||
label,
|
||||
@ -28,7 +30,7 @@ export const useSelectRule = (option: SelectRuleOption) => {
|
||||
if (!option.props) {
|
||||
option.props = []
|
||||
}
|
||||
return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule])
|
||||
return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...rules])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ export const useFormCreateDesigner = async (designer: Ref) => {
|
||||
*/
|
||||
const buildSystemMenu = () => {
|
||||
// 移除自带的下拉选择器组件,使用 currencySelectRule 替代
|
||||
designer.value?.removeMenuItem('select')
|
||||
designer.value?.removeMenuItem('radio')
|
||||
designer.value?.removeMenuItem('checkbox')
|
||||
// designer.value?.removeMenuItem('select')
|
||||
// designer.value?.removeMenuItem('radio')
|
||||
// designer.value?.removeMenuItem('checkbox')
|
||||
const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
|
||||
const menu: Menu = {
|
||||
name: 'system',
|
||||
|
@ -1,4 +1,3 @@
|
||||
// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
|
||||
export function makeRequiredRule() {
|
||||
return {
|
||||
type: 'Required',
|
||||
@ -17,63 +16,3 @@ export const localeProps = (t, prefix, rules) => {
|
||||
return rule
|
||||
})
|
||||
}
|
||||
|
||||
export function upper(str) {
|
||||
return str.replace(str[0], str[0].toLocaleUpperCase())
|
||||
}
|
||||
|
||||
export function makeOptionsRule(t, to, userOptions) {
|
||||
console.log(userOptions[0])
|
||||
const options = [
|
||||
{ label: t('props.optionsType.struct'), value: 0 },
|
||||
{ label: t('props.optionsType.json'), value: 1 },
|
||||
{ label: '用户数据', value: 2 }
|
||||
]
|
||||
|
||||
const control = [
|
||||
{
|
||||
value: 0,
|
||||
rule: [
|
||||
{
|
||||
type: 'TableOptions',
|
||||
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||
props: { defaultValue: [] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
rule: [
|
||||
{
|
||||
type: 'Struct',
|
||||
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||
props: { defaultValue: [] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
rule: [
|
||||
{
|
||||
type: 'TableOptions',
|
||||
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||
props: { modelValue: [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
options.splice(0, 0)
|
||||
control.push()
|
||||
|
||||
return {
|
||||
type: 'radio',
|
||||
title: t('props.options'),
|
||||
field: '_optionType',
|
||||
value: 0,
|
||||
options,
|
||||
props: {
|
||||
type: 'button'
|
||||
},
|
||||
control
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ const props = defineProps({
|
||||
|
||||
const elRef = ref<ElRef>(null)
|
||||
|
||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||
const isLocal = computed(() => props.icon?.startsWith('svg-icon:'))
|
||||
|
||||
const symbolId = computed(() => {
|
||||
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
|
||||
|
235
src/components/MarkdownView/index.vue
Normal file
235
src/components/MarkdownView/index.vue
Normal file
@ -0,0 +1,235 @@
|
||||
|
||||
<template>
|
||||
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
import {marked} from 'marked'
|
||||
import 'highlight.js/styles/vs2015.min.css'
|
||||
import hljs from 'highlight.js'
|
||||
import {ref} from "vue";
|
||||
|
||||
const {copy} = useClipboard() // 初始化 copy 到粘贴板
|
||||
const contentRef = ref()
|
||||
|
||||
// 代码高亮:https://highlightjs.org/
|
||||
// 转换 markdown:marked
|
||||
|
||||
// marked 渲染器
|
||||
const renderer = {
|
||||
code(code, language, c) {
|
||||
let highlightHtml
|
||||
try {
|
||||
highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
|
||||
} catch (e) {
|
||||
// skip
|
||||
}
|
||||
const copyHtml = `<div id="copy" data-copy='${code}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
// 配置 marked
|
||||
marked.use({
|
||||
renderer: renderer
|
||||
})
|
||||
|
||||
// 渲染的html内容
|
||||
const contentHtml = ref<any>()
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 将 props 变为引用类型
|
||||
const { content } = toRefs(props)
|
||||
|
||||
// 监听 content 变化
|
||||
watch(content, async (newValue, oldValue) => {
|
||||
await renderMarkdown(newValue);
|
||||
})
|
||||
|
||||
// 渲染 markdown
|
||||
const renderMarkdown = async (content: string) => {
|
||||
contentHtml.value = await marked(content)
|
||||
}
|
||||
|
||||
// 组件挂在时
|
||||
onMounted(async () => {
|
||||
// 解析转换 markdown
|
||||
await renderMarkdown(props.content as string);
|
||||
//
|
||||
// 添加 copy 监听
|
||||
contentRef.value.addEventListener('click', (e: any) => {
|
||||
console.log(e)
|
||||
if (e.target.id === 'copy') {
|
||||
copy(e.target?.dataset?.copy)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.markdown-view {
|
||||
font-family: PingFang SC;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6rem;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
color: #3b3e55;
|
||||
max-width: 100%;
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
border-radius: 6px;
|
||||
padding-top: 20px;
|
||||
width: auto;
|
||||
@media screen and (min-width: 1536px) {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1536px) and (min-width: 1024px) {
|
||||
width: calc(100vw - 400px - 64px - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) and (min-width: 768px) {
|
||||
width: calc(100vw - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: calc(100vw - 16px * 2);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
code.hljs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
//margin-bottom: 1rem !important;
|
||||
margin: 0;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* 标题通用格式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--color-G900);
|
||||
margin: 24px 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/* 列表(有序,无序) */
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #3b3e55; // var(--color-CG600);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0 0 20px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
list-style-type: decimal;
|
||||
margin-bottom: 1rem;
|
||||
// 表达式,修复有序列表序号展示不全的问题
|
||||
// &:nth-child(n + 10) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// &:nth-child(n + 100) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
}
|
||||
|
||||
ul > li {
|
||||
list-style-type: disc;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-right: 11px;
|
||||
margin-bottom: 1rem;
|
||||
color: #3b3e55; // var(--color-G900);
|
||||
}
|
||||
|
||||
ol ul,
|
||||
ol ul > li,
|
||||
ul ul,
|
||||
ul ul li {
|
||||
// list-style: circle;
|
||||
font-size: 16px;
|
||||
list-style: none;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul ul ul,
|
||||
ul ul ul li,
|
||||
ol ol,
|
||||
ol ol > li,
|
||||
ol ul ul,
|
||||
ol ul ul > li,
|
||||
ul ol,
|
||||
ul ol > li {
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user