mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(web): add @memoh/icon package and unify brand icon system
Replace FontAwesome/CDN brand icons with local SVG-based Vue components in a new shared @memoh/icon package. Provider icon URLs in conf/providers YAML files are replaced with preset names, intercepted by ProviderIcon component on the frontend. SearchProviderLogo and ChannelIcon components are migrated to @memoh/icon. All icon containers now use a unified circular gray (rounded-full bg-muted) style. Adds wechat and matrix channel icons.
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.1",
|
||||
"@memoh/icon": "workspace:*",
|
||||
"@memoh/sdk": "workspace:*",
|
||||
"@memoh/ui": "workspace:*",
|
||||
"@pinia/colada": "^0.21.1",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<component
|
||||
v-if="iconComponent"
|
||||
:is="iconComponent"
|
||||
:size="size"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<span v-else v-bind="$attrs">{{ fallback }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue'
|
||||
import {
|
||||
Qq,
|
||||
Telegram,
|
||||
Discord,
|
||||
Slack,
|
||||
Feishu,
|
||||
Wechat,
|
||||
Matrix,
|
||||
} from '@memoh/icon'
|
||||
|
||||
const channelIcons: Record<string, Component> = {
|
||||
qq: Qq,
|
||||
telegram: Telegram,
|
||||
discord: Discord,
|
||||
slack: Slack,
|
||||
feishu: Feishu,
|
||||
wechat: Wechat,
|
||||
matrix: Matrix,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
channel: string
|
||||
size?: string | number
|
||||
}>(), {
|
||||
size: '1em',
|
||||
})
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const iconComponent = computed<Component | undefined>(() =>
|
||||
channelIcons[props.channel],
|
||||
)
|
||||
|
||||
const fallback = computed(() =>
|
||||
props.channel.slice(0, 2).toUpperCase(),
|
||||
)
|
||||
</script>
|
||||
@@ -1,31 +1,23 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="showBadge"
|
||||
class="absolute -right-0.5 -bottom-0.5 flex size-4 items-center justify-center overflow-hidden rounded-full bg-muted border border-background text-muted-foreground"
|
||||
class="absolute -right-0.5 -bottom-0.5 flex size-4 items-center justify-center rounded-full bg-muted ring-[1.5px] ring-background text-muted-foreground"
|
||||
:title="channelLabel"
|
||||
role="img"
|
||||
:aria-label="channelLabel"
|
||||
>
|
||||
<img
|
||||
v-if="channelImage"
|
||||
:src="channelImage"
|
||||
alt=""
|
||||
class="size-full object-contain"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
:icon="channelIcon!"
|
||||
class="size-2.5"
|
||||
<ChannelIcon
|
||||
:channel="platformKey"
|
||||
size="1em"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getChannelIcon, getChannelImage } from '@/utils/channel-icons'
|
||||
import ChannelIcon from '@/components/channel-icon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
platform: string
|
||||
@@ -37,13 +29,11 @@ const isWebChannel = computed(() => {
|
||||
const k = platformKey.value
|
||||
return k === 'web' || k === ''
|
||||
})
|
||||
const channelImage = computed(() => getChannelImage(platformKey.value))
|
||||
const channelIcon = computed(() => getChannelIcon(platformKey.value))
|
||||
const channelLabel = computed(() => {
|
||||
if (!platformKey.value) return ''
|
||||
const key = `bots.channels.types.${platformKey.value}`
|
||||
const out = t(key)
|
||||
return out !== key ? out : platformKey.value.charAt(0).toUpperCase() + platformKey.value.slice(1)
|
||||
})
|
||||
const showBadge = computed(() => !isWebChannel.value && (channelImage.value || channelIcon.value))
|
||||
const showBadge = computed(() => !isWebChannel.value)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { Component } from 'vue'
|
||||
import {
|
||||
Anthropic,
|
||||
Azure,
|
||||
AzureColor,
|
||||
Baichuan,
|
||||
BaichuanColor,
|
||||
Bailian,
|
||||
BailianColor,
|
||||
Bedrock,
|
||||
BedrockColor,
|
||||
Claude,
|
||||
ClaudeColor,
|
||||
Cohere,
|
||||
CohereColor,
|
||||
Deepseek,
|
||||
DeepseekColor,
|
||||
Doubao,
|
||||
DoubaoColor,
|
||||
Fireworks,
|
||||
FireworksColor,
|
||||
Gemini,
|
||||
GeminiColor,
|
||||
Google,
|
||||
GoogleBrandColor,
|
||||
GoogleColor,
|
||||
Groq,
|
||||
Huggingface,
|
||||
HuggingfaceColor,
|
||||
Hunyuan,
|
||||
HunyuanColor,
|
||||
Kimi,
|
||||
KimiColor,
|
||||
Lmstudio,
|
||||
Meta,
|
||||
MetaColor,
|
||||
Minimax,
|
||||
MinimaxColor,
|
||||
Mistral,
|
||||
MistralColor,
|
||||
Moonshot,
|
||||
Newapi,
|
||||
NewapiColor,
|
||||
Nvidia,
|
||||
NvidiaColor,
|
||||
Ollama,
|
||||
Openai,
|
||||
Openrouter,
|
||||
Qwen,
|
||||
QwenColor,
|
||||
Siliconcloud,
|
||||
SiliconcloudColor,
|
||||
Spark,
|
||||
SparkColor,
|
||||
Stepfun,
|
||||
StepfunColor,
|
||||
Together,
|
||||
TogetherColor,
|
||||
Vertexai,
|
||||
VertexaiColor,
|
||||
Volcengine,
|
||||
VolcengineColor,
|
||||
Xai,
|
||||
Yi,
|
||||
YiColor,
|
||||
Zhipu,
|
||||
ZhipuColor,
|
||||
} from '@memoh/icon'
|
||||
|
||||
/**
|
||||
* Maps preset icon names (stored in DB) to @memoh/icon Vue components.
|
||||
* The key is the SVG filename without extension (e.g. 'openai', 'deepseek-color').
|
||||
*/
|
||||
export const iconMap: Record<string, Component> = {
|
||||
'openai': Openai,
|
||||
'anthropic': Anthropic,
|
||||
'google': Google,
|
||||
'google-color': GoogleColor,
|
||||
'google-brand-color': GoogleBrandColor,
|
||||
'deepseek': Deepseek,
|
||||
'deepseek-color': DeepseekColor,
|
||||
'groq': Groq,
|
||||
'huggingface': Huggingface,
|
||||
'huggingface-color': HuggingfaceColor,
|
||||
'lmstudio': Lmstudio,
|
||||
'minimax': Minimax,
|
||||
'minimax-color': MinimaxColor,
|
||||
'mistral': Mistral,
|
||||
'mistral-color': MistralColor,
|
||||
'moonshot': Moonshot,
|
||||
'ollama': Ollama,
|
||||
'openrouter': Openrouter,
|
||||
'qwen': Qwen,
|
||||
'qwen-color': QwenColor,
|
||||
'xai': Xai,
|
||||
'claude': Claude,
|
||||
'claude-color': ClaudeColor,
|
||||
'gemini': Gemini,
|
||||
'gemini-color': GeminiColor,
|
||||
'meta': Meta,
|
||||
'meta-color': MetaColor,
|
||||
'cohere': Cohere,
|
||||
'cohere-color': CohereColor,
|
||||
'azure': Azure,
|
||||
'azure-color': AzureColor,
|
||||
'nvidia': Nvidia,
|
||||
'nvidia-color': NvidiaColor,
|
||||
'fireworks': Fireworks,
|
||||
'fireworks-color': FireworksColor,
|
||||
'together': Together,
|
||||
'together-color': TogetherColor,
|
||||
'bedrock': Bedrock,
|
||||
'bedrock-color': BedrockColor,
|
||||
'vertexai': Vertexai,
|
||||
'vertexai-color': VertexaiColor,
|
||||
'baichuan': Baichuan,
|
||||
'baichuan-color': BaichuanColor,
|
||||
'zhipu': Zhipu,
|
||||
'zhipu-color': ZhipuColor,
|
||||
'yi': Yi,
|
||||
'yi-color': YiColor,
|
||||
'stepfun': Stepfun,
|
||||
'stepfun-color': StepfunColor,
|
||||
'kimi': Kimi,
|
||||
'kimi-color': KimiColor,
|
||||
'doubao': Doubao,
|
||||
'doubao-color': DoubaoColor,
|
||||
'spark': Spark,
|
||||
'spark-color': SparkColor,
|
||||
'hunyuan': Hunyuan,
|
||||
'hunyuan-color': HunyuanColor,
|
||||
'bailian': Bailian,
|
||||
'bailian-color': BailianColor,
|
||||
'siliconcloud': Siliconcloud,
|
||||
'siliconcloud-color': SiliconcloudColor,
|
||||
'volcengine': Volcengine,
|
||||
'volcengine-color': VolcengineColor,
|
||||
'newapi': Newapi,
|
||||
'newapi-color': NewapiColor,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<component
|
||||
v-if="iconComponent"
|
||||
:is="iconComponent"
|
||||
:size="size"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<img
|
||||
v-else-if="isUrl"
|
||||
:src="icon"
|
||||
:width="size"
|
||||
:height="size"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue'
|
||||
import { iconMap } from './icons.ts'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
icon: string
|
||||
size?: string | number
|
||||
}>(), {
|
||||
size: '1em',
|
||||
})
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const isUrl = computed(() =>
|
||||
props.icon.startsWith('http://') || props.icon.startsWith('https://'),
|
||||
)
|
||||
|
||||
const iconComponent = computed<Component | undefined>(() => {
|
||||
if (isUrl.value) return undefined
|
||||
return iconMap[props.icon]
|
||||
})
|
||||
</script>
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Search provider icon registry (FontAwesome).
|
||||
*
|
||||
* To add a new provider icon:
|
||||
* 1. Find the icon in FontAwesome (https://fontawesome.com/icons)
|
||||
* or add a custom definition in custom-icons.ts
|
||||
* 2. Import it in `main.ts` and add to `library.add()`
|
||||
* 3. Add the [prefix, iconName] tuple to PROVIDER_ICONS below
|
||||
*
|
||||
* The key must match the `provider` field stored in the database (lowercase).
|
||||
*/
|
||||
|
||||
const PROVIDER_ICONS: Record<string, [string, string]> = {
|
||||
brave: ['fab', 'brave'],
|
||||
bing: ['fab', 'microsoft'],
|
||||
google: ['fab', 'google'],
|
||||
yandex: ['fab', 'yandex'],
|
||||
tavily: ['fac', 'tavily'],
|
||||
jina: ['fac', 'jina'],
|
||||
exa: ['fac', 'exa'],
|
||||
bocha: ['fac', 'bocha'],
|
||||
duckduckgo: ['fac', 'duckduckgo'],
|
||||
searxng: ['fac', 'searxng'],
|
||||
sogou: ['fac', 'sogou'],
|
||||
serper: ['fac', 'serper'],
|
||||
}
|
||||
|
||||
const DEFAULT_ICON: [string, string] = ['fas', 'globe']
|
||||
|
||||
export function getSearchProviderIcon(provider: string): [string, string] {
|
||||
if (!provider) return DEFAULT_ICON
|
||||
return PROVIDER_ICONS[provider.trim().toLowerCase()] ?? DEFAULT_ICON
|
||||
}
|
||||
@@ -1,33 +1,99 @@
|
||||
<template>
|
||||
<FontAwesomeIcon
|
||||
:icon="icon"
|
||||
class="shrink-0"
|
||||
:class="sizeClass"
|
||||
/>
|
||||
<span
|
||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground"
|
||||
:class="containerClass"
|
||||
>
|
||||
<component
|
||||
v-if="iconComponent"
|
||||
:is="iconComponent"
|
||||
:size="iconSize"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
:icon="['fas', 'globe']"
|
||||
:class="iconSizeClass"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getSearchProviderIcon } from './icons'
|
||||
import { computed, type Component } from 'vue'
|
||||
import {
|
||||
Brave,
|
||||
Bing,
|
||||
BingColor,
|
||||
Google,
|
||||
GoogleColor,
|
||||
Yandex,
|
||||
Tavily,
|
||||
TavilyColor,
|
||||
Jina,
|
||||
Exa,
|
||||
ExaColor,
|
||||
Bocha,
|
||||
Duckduckgo,
|
||||
Searxng,
|
||||
Sogou,
|
||||
Serper,
|
||||
} from '@memoh/icon'
|
||||
|
||||
const searchIcons: Record<string, Component> = {
|
||||
brave: Brave,
|
||||
bing: BingColor,
|
||||
'bing-mono': Bing,
|
||||
google: GoogleColor,
|
||||
'google-mono': Google,
|
||||
yandex: Yandex,
|
||||
tavily: TavilyColor,
|
||||
'tavily-mono': Tavily,
|
||||
jina: Jina,
|
||||
exa: ExaColor,
|
||||
'exa-mono': Exa,
|
||||
bocha: Bocha,
|
||||
duckduckgo: Duckduckgo,
|
||||
searxng: Searxng,
|
||||
sogou: Sogou,
|
||||
serper: Serper,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** The provider key, e.g. 'brave' */
|
||||
provider: string
|
||||
/** Size preset */
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||
}>(), {
|
||||
size: 'sm',
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
const iconComponent = computed<Component | undefined>(() =>
|
||||
searchIcons[props.provider?.trim().toLowerCase()],
|
||||
)
|
||||
|
||||
const containerClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'xs': return 'size-3'
|
||||
case 'xs': return 'size-5'
|
||||
case 'sm': return 'size-7'
|
||||
case 'md': return 'size-8'
|
||||
case 'lg': return 'size-10'
|
||||
default: return 'size-7'
|
||||
}
|
||||
})
|
||||
|
||||
const iconSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'xs': return '0.625em'
|
||||
case 'sm': return '1em'
|
||||
case 'md': return '1.25em'
|
||||
case 'lg': return '1.5em'
|
||||
default: return '1em'
|
||||
}
|
||||
})
|
||||
|
||||
const iconSizeClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'xs': return 'size-2.5'
|
||||
case 'sm': return 'size-3.5'
|
||||
case 'md': return 'size-4'
|
||||
case 'lg': return 'size-5'
|
||||
default: return 'size-3.5'
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() => getSearchProviderIcon(props.provider))
|
||||
</script>
|
||||
|
||||
@@ -41,12 +41,9 @@
|
||||
:class="{ 'bg-accent': selectedType === item.meta.type }"
|
||||
@click="selectedType = item.meta.type as string"
|
||||
>
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-md text-xs font-bold uppercase"
|
||||
:class="channelBadgeClass(item.meta.type as string)"
|
||||
>
|
||||
{{ channelIcon(item.meta.type) }}
|
||||
</div>
|
||||
<span class="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<ChannelIcon :channel="item.meta.type as string" size="1.25em" />
|
||||
</span>
|
||||
<div class="flex-1 text-left">
|
||||
<div class="font-medium">
|
||||
{{ item.meta.display_name }}
|
||||
@@ -103,12 +100,9 @@
|
||||
class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors"
|
||||
@click="addChannel(item.meta.type)"
|
||||
>
|
||||
<div
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md text-xs font-bold uppercase"
|
||||
:class="channelBadgeClass(item.meta.type)"
|
||||
>
|
||||
{{ channelIcon(item.meta.type) }}
|
||||
</div>
|
||||
<span class="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<ChannelIcon :channel="item.meta.type" size="1em" />
|
||||
</span>
|
||||
<span>{{ item.meta.display_name }}</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
@@ -148,6 +142,7 @@ import { useQuery } from '@pinia/colada'
|
||||
import { getChannels, getBotsByIdChannelByPlatform } from '@memoh/sdk'
|
||||
import type { HandlersChannelMeta, ChannelChannelConfig } from '@memoh/sdk'
|
||||
import ChannelSettingsPanel from './channel-settings-panel.vue'
|
||||
import ChannelIcon from '@/components/channel-icon/index.vue'
|
||||
|
||||
export interface BotChannelItem {
|
||||
meta: HandlersChannelMeta
|
||||
@@ -219,23 +214,4 @@ function addChannel(type: string) {
|
||||
selectedType.value = type
|
||||
}
|
||||
|
||||
function channelIcon(type: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
qq: 'QQ',
|
||||
telegram: 'TG',
|
||||
matrix: 'MX',
|
||||
feishu: '飞',
|
||||
}
|
||||
return icons[type] ?? type.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
function channelBadgeClass(type: string): string {
|
||||
const classes: Record<string, string> = {
|
||||
qq: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
||||
telegram: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
||||
matrix: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
feishu: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300',
|
||||
}
|
||||
return classes[type] ?? 'bg-secondary text-secondary-foreground'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ channelItem.meta.display_name }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ channelItem.meta.type }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<ChannelIcon :channel="channelItem.meta.type" size="1.5em" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ channelItem.meta.display_name }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ channelItem.meta.type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="isEditMode">
|
||||
@@ -253,6 +258,7 @@ import { putBotsByIdChannelByPlatform, deleteBotsByIdChannelByPlatform, patchBot
|
||||
import type { HandlersChannelMeta, ChannelChannelConfig, ChannelFieldSchema, ChannelUpsertConfigRequest } from '@memoh/sdk'
|
||||
import { client } from '@memoh/sdk/client'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import ChannelIcon from '@/components/channel-icon/index.vue'
|
||||
|
||||
interface BotChannelItem {
|
||||
meta: HandlersChannelMeta
|
||||
|
||||
@@ -16,13 +16,11 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
Button,
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
} from '@memoh/ui'
|
||||
import { getProviders } from '@memoh/sdk'
|
||||
import type { ProvidersGetResponse } from '@memoh/sdk'
|
||||
import AddProvider from '@/components/add-provider/index.vue'
|
||||
import ProviderIcon from '@/components/provider-icon/index.vue'
|
||||
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
|
||||
|
||||
function getInitials(name: string | undefined) {
|
||||
@@ -131,15 +129,16 @@ const openStatus = reactive({
|
||||
}"
|
||||
>
|
||||
<span class="relative shrink-0">
|
||||
<Avatar class="size-7">
|
||||
<AvatarImage
|
||||
<span class="flex size-7 items-center justify-center rounded-full bg-muted">
|
||||
<ProviderIcon
|
||||
v-if="providerItem.icon"
|
||||
:src="providerItem.icon"
|
||||
:icon="providerItem.icon"
|
||||
size="1.25em"
|
||||
/>
|
||||
<AvatarFallback class="text-xs font-medium">
|
||||
<span v-else class="text-xs font-medium text-muted-foreground">
|
||||
{{ getInitials(providerItem.name) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="providerItem.enable !== false"
|
||||
class="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full bg-green-500 ring-2 ring-background"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<section class="flex items-center gap-3">
|
||||
<Avatar class="size-10 shrink-0">
|
||||
<AvatarImage
|
||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<ProviderIcon
|
||||
v-if="curProvider?.icon"
|
||||
:src="curProvider.icon"
|
||||
:icon="curProvider.icon"
|
||||
size="1.5em"
|
||||
/>
|
||||
<AvatarFallback class="text-sm font-medium">
|
||||
<span v-else class="text-sm font-medium text-muted-foreground">
|
||||
{{ getInitials(curProvider?.name) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</span>
|
||||
</span>
|
||||
<h4 class="scroll-m-20 tracking-tight">
|
||||
{{ curProvider?.name }}
|
||||
</h4>
|
||||
@@ -37,7 +38,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Separator, Avatar, AvatarImage, AvatarFallback } from '@memoh/ui'
|
||||
import { Separator } from '@memoh/ui'
|
||||
import ProviderIcon from '@/components/provider-icon/index.vue'
|
||||
|
||||
function getInitials(name: string | undefined) {
|
||||
const label = name?.trim() ?? ''
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
const LOCAL_CHANNEL_IMAGES: Record<string, string> = {
|
||||
feishu: '/channels/feishu.png',
|
||||
matrix: '/channels/matrix.svg',
|
||||
telegram: '/channels/telegram.webp',
|
||||
}
|
||||
|
||||
const CHANNEL_ICONS: Record<string, [string, string]> = {
|
||||
qq: ['fab', 'qq'],
|
||||
telegram: ['fab', 'telegram'],
|
||||
matrix: ['fas', 'hashtag'],
|
||||
feishu: ['fas', 'comment-dots'],
|
||||
web: ['fas', 'globe'],
|
||||
slack: ['fab', 'slack'],
|
||||
discord: ['fab', 'discord'],
|
||||
email: ['fas', 'envelope'],
|
||||
}
|
||||
|
||||
const DEFAULT_ICON: [string, string] = ['far', 'comment']
|
||||
|
||||
export function getChannelIcon(platformKey: string): [string, string] {
|
||||
if (!platformKey) return DEFAULT_ICON
|
||||
return CHANNEL_ICONS[platformKey] ?? DEFAULT_ICON
|
||||
}
|
||||
|
||||
export function getChannelImage(platformKey: string): string | null {
|
||||
if (!platformKey) return null
|
||||
return LOCAL_CHANNEL_IMAGES[platformKey] ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user