This commit is contained in:
zlwu committed 2026-05-17 08:36:37 +08:00
1 parent 246ad2228e
commit 8e2dd8b9f0
186 files changed
+38821 -2571

No files matched your search

@@ -0,0 +1,211 @@
<!-- 通用查看弹窗 —— 样式对齐待办中心「查看详情」 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
v-model="innerVisible"
width="720px"
class="system-view-dialog"
align-center
destroy-on-close
:show-close="false"
style="backdrop-filter: none"
>
<div class="dialog-scroll">
<!-- hero -->
<div class="view-hero">
<div class="view-hero-icon">
<el-icon :size="28"><component :is="iconComponent" /></el-icon>
</div>
<div class="view-hero-main">
<h2>{{ title }}</h2>
<p v-if="meta" class="view-hero-meta">{{ meta }}</p>
</div>
</div>
<!-- sections -->
<div v-for="section in sections" :key="section.title" class="view-section">
<h3>{{ section.title }}</h3>
<dl class="view-kv-grid">
<div
v-for="field in section.fields"
:key="field.label"
class="view-kv"
:class="{ 'view-kv-span-2': field.span }"
>
<dt>{{ field.label }}</dt>
<dd>{{ field.value ?? '—' }}</dd>
</div>
</dl>
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="innerVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, type Component } from 'vue'
import { Close } from '@element-plus/icons-vue'
export interface ViewField {
label: string
value: string | number | boolean | null | undefined
span?: boolean
}
export interface ViewSection {
title: string
fields: ViewField[]
}
const props = defineProps<{
visible: boolean
title: string
icon?: Component
meta?: string
sections: ViewSection[]
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const iconComponent = computed(() => props.icon || (() => null))
</script>
<style scoped lang="scss">
.dialog-scroll {
overflow-y: auto;
max-height: calc(100vh - 120px);
}
.view-hero {
display: flex;
gap: 16px;
padding: 20px 22px;
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 55%);
border-bottom: 1px solid #d9e8e2;
align-items: center;
}
.view-hero-icon {
width: 72px;
height: 72px;
border-radius: 12px;
border: 1px solid rgba(16, 185, 129, 0.2);
background: rgba(16, 185, 129, 0.08);
color: #059669;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.view-hero-main {
flex: 1;
min-width: 0;
h2 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 700;
color: #1e293b;
letter-spacing: -0.02em;
}
}
.view-hero-meta {
margin: 0;
font-size: 13px;
color: #64748b;
line-height: 1.5;
}
.view-section {
padding: 18px 22px;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
h3 {
margin: 0 0 14px;
padding-left: 10px;
font-size: 14px;
font-weight: 600;
color: #1e293b;
border-left: 3px solid #10b981;
line-height: 1;
}
}
.view-kv-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 28px;
}
.view-kv {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
font-size: 13px;
align-items: start;
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
word-break: break-word;
}
}
.view-kv-span-2 {
grid-column: 1 / -1;
}
@media (max-width: 640px) {
.view-kv-grid {
grid-template-columns: 1fr;
}
.view-kv {
grid-template-columns: 1fr;
gap: 4px;
}
}
</style>
<style lang="scss">
.system-view-dialog {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
overflow: hidden;
}
.el-dialog__footer {
padding: 12px 22px;
border-top: 1px solid #d9e8e2;
background: #f8fafc;
}
}
</style>
@@ -107,7 +107,6 @@
ElDatePicker,
ElInput,
ElInputTag,
ElInputNumber,
ElRadioGroup,
ElRate,
ElSelect,
@@ -125,7 +124,7 @@
const componentMap = {
input: ElInput, // 输入框
inputtag: ElInputTag, // 标签输入框
number: ElInputNumber, // 数字输入框
number: ElInput, // 数字输入框
select: ElSelect, // 选择器
switch: ElSwitch, // 开关
checkbox: ElCheckbox, // 复选框
@@ -414,9 +413,22 @@
}
const getProps = (item: FormItem) => {
if (item.props) return item.props
if (item.props) {
// number 类型使用 ElInput 渲染时需要注入 type="number"
if (item.type === 'number') {
return { type: 'number', step: 'any', ...item.props }
}
return item.props
}
const props = { ...item }
rootProps.forEach((key) => delete (props as Record<string, any>)[key])
// number 类型使用 ElInput 渲染时需要注入 type="number"
if (item.type === 'number') {
;(props as Record<string, any>).type = 'number'
if (!(props as Record<string, any>).step) {
;(props as Record<string, any>).step = 'any'
}
}
return props
}
@@ -114,7 +114,6 @@
ElDatePicker,
ElInput,
ElInputTag,
ElInputNumber,
ElRadioGroup,
ElRate,
ElSelect,
@@ -132,7 +131,7 @@
const componentMap = {
input: ElInput, // 输入框
inputTag: ElInputTag, // 标签输入框
number: ElInputNumber, // 数字输入框
number: ElInput, // 数字输入框
select: ElSelect, // 选择器
switch: ElSwitch, // 开关
checkbox: ElCheckbox, // 复选框
@@ -297,9 +296,22 @@
}))
const getProps = (item: SearchFormItem) => {
if (item.props) return item.props
if (item.props) {
// number 类型使用 ElInput 渲染时需要注入 type="number"
if (item.type === 'number') {
return { type: 'number', step: 'any', ...item.props }
}
return item.props
}
const props = { ...item }
rootProps.forEach((key) => delete (props as Record<string, any>)[key])
// number 类型使用 ElInput 渲染时需要注入 type="number"
if (item.type === 'number') {
;(props as Record<string, any>).type = 'number'
if (!(props as Record<string, any>).step) {
;(props as Record<string, any>).step = 'any'
}
}
return props
}
@@ -36,12 +36,20 @@
}"
>
<ArtSvgIcon
v-if="menu.meta.icon"
class="menu-icon text-g-700 dark:text-g-800"
:icon="menu.meta.icon"
:style="{
marginBottom: dualMenuShowText ? '5px' : '0'
}"
/>
<div
v-else
class="dual-menu-icon-placeholder"
:style="{
marginBottom: dualMenuShowText ? '5px' : '0'
}"
/>
<span v-if="dualMenuShowText" class="text-md text-g-700">
{{ $t(menu.meta.title) }}
</span>
@@ -55,6 +55,14 @@
font-size: 20px;
}
.dual-menu-icon-placeholder {
display: block;
flex-shrink: 0;
width: 20px;
height: 20px;
margin: 0 auto;
}
span {
display: -webkit-box;
width: 100%;
@@ -112,12 +112,25 @@ $popup-menu-radius: 6px;
}
}
// 菜单图标
// 菜单图标(固定槽宽,无 icon 的一级项用空槽与文字对齐)
.menu-icon {
box-sizing: border-box;
flex-shrink: 0;
width: $menu-icon-size;
min-width: $menu-icon-size;
height: $menu-icon-size;
margin-right: 8px;
font-size: $menu-icon-size;
}
.menu-icon__virtual {
display: block;
flex: 0 0 0;
width: 0;
height: 0;
overflow: hidden;
}
// 菜单高度
.el-sub-menu__title,
.el-menu-item {
@@ -2,12 +2,17 @@
<template v-for="(item, index) in filteredMenuItems" :key="getUniqueKey(item, index)">
<ElSubMenu v-if="hasChildren(item)" :index="item.path || item.meta.title" :level="level">
<template #title>
<div v-if="level === 0" class="menu-icon flex-cc">
<div
v-if="level === 0 && (item.meta.icon || item.meta.virtualMenuIcon)"
class="menu-icon flex-cc"
>
<ArtSvgIcon
v-if="item.meta.icon"
:icon="item.meta.icon"
:color="theme?.iconColor"
:style="{ color: theme.iconColor }"
/>
<span v-else class="menu-icon__virtual" aria-hidden="true" />
</div>
<span class="menu-name">
{{ formatMenuTitle(item.meta.title) }}
@@ -30,12 +35,17 @@
:level-item="level + 1"
@click="goPage(item)"
>
<div v-if="level === 0" class="menu-icon flex-cc">
<div
v-if="level === 0 && (item.meta.icon || item.meta.virtualMenuIcon)"
class="menu-icon flex-cc"
>
<ArtSvgIcon
v-if="item.meta.icon"
:icon="item.meta.icon"
:color="theme?.iconColor"
:style="{ color: theme.iconColor }"
/>
<span v-else class="menu-icon__virtual" aria-hidden="true" />
</div>
<div
v-show="item.meta.showBadge && level === 0 && !menuOpen"
@@ -6,15 +6,15 @@
<ElSwitch v-if="config.type === 'switch'" :model-value="modelValue" @change="handleChange" />
<!-- 数字输入类型 -->
<ElInputNumber
<ElInput
type="number"
step="any"
v-else-if="config.type === 'input-number'"
:model-value="modelValue"
:min="config.min"
:max="config.max"
:step="config.step"
:style="config.style"
:controls-position="config.controlsPosition"
@change="handleChange"
@input="handleChange"
/>
<!-- 选择器类型 -->
@@ -2,9 +2,9 @@
<template>
<div
v-if="showWorkTab"
class="worktab-strip box-border flex-b w-full px-5 mb-3 select-none max-sm:px-[15px]"
class="worktab-strip box-border flex w-full items-center px-5 mb-3 select-none max-sm:px-[15px]"
>
<div class="worktab-scroll-wrap w-full" ref="scrollRef">
<div class="worktab-scroll-wrap min-w-0 flex-1" ref="scrollRef">
<ul
class="float-left whitespace-nowrap !bg-transparent flex"
:class="[tabStyle === 'tab-google' ? 'pl-1' : '']"
@@ -24,12 +24,6 @@
@click="clickTab(item)"
@contextmenu.prevent="(e) => showMenu(e, item.path)"
>
<ArtSvgIcon
v-show="item.icon"
:icon="item.icon"
class="text-base mr-1 group-hover:text-theme"
:class="item.path === activeTab ? 'text-theme' : 'text-inherit'"
/>
{{ item.customTitle || formatMenuTitle(item.title) }}
<span
v-if="list.length > 1 && !item.fixedTab"
@@ -42,7 +36,7 @@
</ul>
</div>
<div class="flex">
<div class="flex shrink-0">
<div
class="worktab-menu-btn flex-cc relative top-0 size-8 leading-8 text-center c-p tad-200"
@click="(e) => showMenu(e, activeTab)"