feat(web-admin): add OaAppIconPicker for ep palette and upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e4279dc953
commit
0cbfa7bb91
2 files changed
+347
-5
No files matched your search
@@ -9,8 +9,52 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue'
|
||||
import * as EpIcons from '@element-plus/icons-vue'
|
||||
import { OA_APP_DEFAULT_EP_ICON, resolveOaAppIcon } from '@/utils/oa-app-icons'
|
||||
import {
|
||||
Bell,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
Checked,
|
||||
Clock,
|
||||
CollectionTag,
|
||||
CreditCard,
|
||||
Document,
|
||||
Location,
|
||||
Message,
|
||||
Money,
|
||||
Notebook,
|
||||
OfficeBuilding,
|
||||
Present,
|
||||
Setting,
|
||||
Stamp,
|
||||
Tickets,
|
||||
User,
|
||||
Van,
|
||||
Wallet
|
||||
} from '@element-plus/icons-vue'
|
||||
import { resolveOaAppIcon, type OaAppEpIconName } from '@/utils/oa-app-icons'
|
||||
|
||||
const epMap: Record<OaAppEpIconName, Component> = {
|
||||
Document,
|
||||
Calendar,
|
||||
Clock,
|
||||
Wallet,
|
||||
User,
|
||||
Tickets,
|
||||
Briefcase,
|
||||
Money,
|
||||
Van,
|
||||
OfficeBuilding,
|
||||
Message,
|
||||
Setting,
|
||||
CollectionTag,
|
||||
Checked,
|
||||
Present,
|
||||
Stamp,
|
||||
CreditCard,
|
||||
Notebook,
|
||||
Bell,
|
||||
Location
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -24,9 +68,9 @@
|
||||
const resolved = computed(() => resolveOaAppIcon(props.iconUrl))
|
||||
|
||||
const epComponent = computed<Component>(() => {
|
||||
if (resolved.value.kind !== 'ep') return (EpIcons as any)[OA_APP_DEFAULT_EP_ICON]
|
||||
const name = resolved.value.icon
|
||||
return (EpIcons as any)[name] || (EpIcons as any)[OA_APP_DEFAULT_EP_ICON]
|
||||
if (resolved.value.kind !== 'ep') return Document
|
||||
const name = resolved.value.icon as OaAppEpIconName
|
||||
return epMap[name] || Document
|
||||
})
|
||||
|
||||
const boxStyle = computed(() => {
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="oa-icon-picker">
|
||||
<OaAppIcon :icon-url="modelValue" size="md" />
|
||||
<div class="oa-icon-picker__actions">
|
||||
<el-button class="btn-ghost" type="button" @click="panelOpen = !panelOpen">
|
||||
<el-icon><Grid /></el-icon>
|
||||
从图标库选择
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" type="button" @click="onRecolor">
|
||||
<el-icon><Brush /></el-icon>
|
||||
换底色
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" type="button" @click="onRandom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
随机图标+底色
|
||||
</el-button>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:http-request="uploadIcon"
|
||||
:disabled="uploading"
|
||||
>
|
||||
<el-button class="btn-ghost" :loading="uploading">
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传图片
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<p class="oa-icon-picker__tip">
|
||||
新增时随机淡色底 + EP 图标(同色系深色);可展开选图标/换底色,或上传自定义图
|
||||
</p>
|
||||
</div>
|
||||
<div v-show="panelOpen" class="oa-icon-picker__panel">
|
||||
<div class="oa-icon-picker__panel-head">
|
||||
<strong>Element Plus 图标(精选)</strong>
|
||||
<el-button class="btn-ghost" style="height: 24px" @click="panelOpen = false">收起</el-button>
|
||||
</div>
|
||||
<div class="oa-icon-picker__grid">
|
||||
<button
|
||||
v-for="name in OA_APP_EP_ICON_NAMES"
|
||||
:key="name"
|
||||
type="button"
|
||||
class="oa-icon-picker__cell"
|
||||
:class="{ active: currentEp.icon === name && isEp }"
|
||||
@click="onPickIcon(name)"
|
||||
>
|
||||
<el-icon :style="{ color: currentEp.fg }"><component :is="epMap[name]" /></el-icon>
|
||||
<span>{{ name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-label" style="margin-top: 10px; font-size: 12px; color: var(--mint-text-muted, #94a3b8)">
|
||||
底色(8 组彩色,无灰色)
|
||||
</div>
|
||||
<div class="oa-icon-picker__colors">
|
||||
<button
|
||||
v-for="pair in OA_APP_ICON_COLOR_PAIRS"
|
||||
:key="pair.bg"
|
||||
type="button"
|
||||
class="oa-icon-picker__dot"
|
||||
:class="{ active: currentEp.bg === pair.bg && isEp }"
|
||||
:style="{ background: pair.bg }"
|
||||
:title="pair.bg"
|
||||
@click="onPickBg(pair.bg)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, type Component } from 'vue'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import {
|
||||
Bell,
|
||||
Briefcase,
|
||||
Brush,
|
||||
Calendar,
|
||||
Checked,
|
||||
Clock,
|
||||
CollectionTag,
|
||||
CreditCard,
|
||||
Document,
|
||||
Grid,
|
||||
Location,
|
||||
Message,
|
||||
Money,
|
||||
Notebook,
|
||||
OfficeBuilding,
|
||||
Present,
|
||||
Refresh,
|
||||
Setting,
|
||||
Stamp,
|
||||
Tickets,
|
||||
Upload,
|
||||
User,
|
||||
Van,
|
||||
Wallet
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import OaAppIcon from '@/components/oa-approval/OaAppIcon.vue'
|
||||
import {
|
||||
OA_APP_EP_ICON_NAMES,
|
||||
OA_APP_ICON_COLOR_PAIRS,
|
||||
buildEpIconUrl,
|
||||
isEpIconUrl,
|
||||
parseEpIconUrl,
|
||||
pickRandomOaAppIconUrl,
|
||||
recolorOaAppIconUrl,
|
||||
type OaAppEpIconName
|
||||
} from '@/utils/oa-app-icons'
|
||||
|
||||
const props = defineProps<{ modelValue?: string | null }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
||||
|
||||
const panelOpen = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
const isEp = computed(() => isEpIconUrl(props.modelValue))
|
||||
const currentEp = computed(() => parseEpIconUrl(props.modelValue))
|
||||
|
||||
const epMap: Record<OaAppEpIconName, Component> = {
|
||||
Document,
|
||||
Calendar,
|
||||
Clock,
|
||||
Wallet,
|
||||
User,
|
||||
Tickets,
|
||||
Briefcase,
|
||||
Money,
|
||||
Van,
|
||||
OfficeBuilding,
|
||||
Message,
|
||||
Setting,
|
||||
CollectionTag,
|
||||
Checked,
|
||||
Present,
|
||||
Stamp,
|
||||
CreditCard,
|
||||
Notebook,
|
||||
Bell,
|
||||
Location
|
||||
}
|
||||
|
||||
function setUrl(v: string) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
|
||||
function onPickIcon(name: string) {
|
||||
setUrl(buildEpIconUrl(name, currentEp.value.bg))
|
||||
}
|
||||
|
||||
function onPickBg(bg: string) {
|
||||
setUrl(recolorOaAppIconUrl(props.modelValue, bg))
|
||||
}
|
||||
|
||||
function onRecolor() {
|
||||
const list = OA_APP_ICON_COLOR_PAIRS
|
||||
const cur = currentEp.value.bg
|
||||
let next = list[Math.floor(Math.random() * list.length)].bg
|
||||
if (list.length > 1) {
|
||||
let guard = 0
|
||||
while (next === cur && guard++ < 8) {
|
||||
next = list[Math.floor(Math.random() * list.length)].bg
|
||||
}
|
||||
}
|
||||
setUrl(recolorOaAppIconUrl(props.modelValue, next))
|
||||
}
|
||||
|
||||
function onRandom() {
|
||||
setUrl(pickRandomOaAppIconUrl())
|
||||
}
|
||||
|
||||
async function uploadIcon(options: UploadRequestOptions) {
|
||||
const file = options.file as File
|
||||
if (!file?.type?.startsWith('image/')) {
|
||||
ElMessage.warning('请上传图片文件')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
const url = res?.fileUrl || res?.data?.fileUrl
|
||||
if (!url) {
|
||||
ElMessage.error('上传失败,未返回地址')
|
||||
return
|
||||
}
|
||||
setUrl(url)
|
||||
options.onSuccess?.(res)
|
||||
} catch (e) {
|
||||
options.onError?.(e as any)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.oa-icon-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oa-icon-picker__actions {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.oa-icon-picker__tip {
|
||||
flex-basis: 100%;
|
||||
margin: 0;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-icon-picker__panel {
|
||||
flex-basis: 100%;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.oa-icon-picker__panel-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-icon-picker__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||
gap: 8px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.oa-icon-picker__cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
padding: 4px;
|
||||
color: #475569;
|
||||
font-size: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-dark, #059669);
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary-dark, #059669);
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
border-color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
.oa-icon-picker__colors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.oa-icon-picker__dot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 0 0 1px rgb(15 23 42 / 6%);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-dark, #059669);
|
||||
box-shadow: 0 0 0 2px rgb(16 185 129 / 25%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user