This commit is contained in:
oneao committed 2025-12-17 22:31:53 +08:00
1 parent ff327ba2fd
commit 1217f7df5f
10 files changed
+503 -6

No files matched your search

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed, useAttrs } from 'vue'
interface Props {
/** 图标名称 */
icon?: string
/** 图标颜色 */
color?: string
/** 图标大小(px) */
fontSize?: number | string
}
const props = defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed(() => {
const style: Record<string, string> = {}
if (props.color) {
style.color = props.color
}
if (props.fontSize) {
style.fontSize
= typeof props.fontSize === 'number'
? `${props.fontSize}px`
: props.fontSize
}
return {
...attrs,
style: {
...(attrs.style as Record<string, string>),
...style,
},
}
})
</script>
<template>
<Icon
v-if="icon"
:icon="icon"
v-bind="bindAttrs"
/>
</template>
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { NavMenuItem } from './type'
import SvgIcon from '@/components/SvgIcon.vue'
defineOptions({
name: 'NavMenu',
})
defineProps<{
menu: NavMenuItem []
}>()
</script>
<template>
<template v-for="item in menu" :key="item.index">
<!-- 有子菜单 -->
<el-sub-menu v-if="item.children?.length" :index="item.index">
<template #title>
<el-icon v-if="item.icon">
<SvgIcon :icon="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</template>
<!-- 递归:注意这里一定是数组 -->
<NavMenu :menu="item.children" />
</el-sub-menu>
<!-- 单菜单 -->
<el-menu-item v-else :index="item.index">
<el-icon v-if="item.icon">
<SvgIcon :icon="item.icon" />
</el-icon>
{{ item.label }}
</el-menu-item>
</template>
</template>
@@ -0,0 +1,6 @@
export interface NavMenuItem {
label: string
index: string
icon?: string
children?: NavMenuItem []
}
@@ -22,6 +22,16 @@ const router = createRouter({
path: '/company/detail',
component: () => import('@/views/company/detail/index.vue'),
},
{
path: '/settings',
component: () => import('@/views/settings/index.vue'),
children: [
{
path: '/settings/info/company',
component: () => import('@/views/settings/info/company.vue'),
},
],
},
],
},
{
@@ -16,10 +16,20 @@ declare module 'vue' {
HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
ICarbonAccessibility: typeof import('~icons/carbon/accessibility')['default']
'IFa7Solid:userShield': typeof import('~icons/fa7-solid/user-shield')['default']
'IHugeicons:building02': typeof import('~icons/hugeicons/building02')['default']
'IHugeicons:edit01': typeof import('~icons/hugeicons/edit01')['default']
'IHugeicons:edit04': typeof import('~icons/hugeicons/edit04')['default']
'IHugeicons:home03': typeof import('~icons/hugeicons/home03')['default']
'IHugeicons:materialAndTexture': typeof import('~icons/hugeicons/material-and-texture')['default']
'IHugeicons:setting07': typeof import('~icons/hugeicons/setting07')['default']
'IHugeicons:userSettings01': typeof import('~icons/hugeicons/user-settings01')['default']
'IIc:outlineSettings': typeof import('~icons/ic/outline-settings')['default']
'IIc:twotoneHeadphones': typeof import('~icons/ic/twotone-headphones')['default']
'IIconoir:infoCircleSolid': typeof import('~icons/iconoir/info-circle-solid')['default']
'IMajesticons:settingsCogLine': typeof import('~icons/majesticons/settings-cog-line')['default']
'IMaterialSymbols:call': typeof import('~icons/material-symbols/call')['default']
'IMaterialSymbols:chatBubbleOutline': typeof import('~icons/material-symbols/chat-bubble-outline')['default']
'IMaterialSymbols:databaseOutline': typeof import('~icons/material-symbols/database-outline')['default']
'IMaterialSymbols:folderCopyOutline': typeof import('~icons/material-symbols/folder-copy-outline')['default']
'IMaterialSymbols:fullscreen': typeof import('~icons/material-symbols/fullscreen')['default']
'IMaterialSymbols:fullscreenExit': typeof import('~icons/material-symbols/fullscreen-exit')['default']
@@ -38,18 +48,25 @@ declare module 'vue' {
'IRi:information2Line': typeof import('~icons/ri/information2-line')['default']
'IRi:informationFill': typeof import('~icons/ri/information-fill')['default']
'IRi:message3Fill': typeof import('~icons/ri/message3-fill')['default']
'ISolar:buildings2Broken': typeof import('~icons/solar/buildings2-broken')['default']
'ISolar:chatRoundBold': typeof import('~icons/solar/chat-round-bold')['default']
'ISolar:chatRoundCallBroken': typeof import('~icons/solar/chat-round-call-broken')['default']
'ISolar:databaseBoldDuotone': typeof import('~icons/solar/database-bold-duotone')['default']
'ISolar:eyeBroken': typeof import('~icons/solar/eye-broken')['default']
'ISolar:eyeClosedBroken': typeof import('~icons/solar/eye-closed-broken')['default']
'IStreamlineUltimate:informationCircleBold': typeof import('~icons/streamline-ultimate/information-circle-bold')['default']
'ITdesign:notification': typeof import('~icons/tdesign/notification')['default']
'ITdesign:search': typeof import('~icons/tdesign/search')['default']
'ITdesign:setting1': typeof import('~icons/tdesign/setting1')['default']
Menu: typeof import('../components/nav-menu/index.vue')['default']
MenuItem: typeof import('../components/nav-menu/index.vue')['default']
NavMenu: typeof import('./../components/nav-menu/index.vue')['default']
Preview: typeof import('./../components/wang-editor/modules/preview.vue')['default']
QueryCompany: typeof import('./../components/QueryCompany.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SafeImage: typeof import('./../components/SafeImage.vue')['default']
SvgIcon: typeof import('./../components/SvgIcon.vue')['default']
UploadProgress: typeof import('./../components/upload-progress/index.vue')['default']
WangEditor: typeof import('./../components/wang-editor/index.vue')['default']
}
@@ -0,0 +1,56 @@
import { h } from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
/**
* 图标渲染
* - 用于vue的render函数
*/
export function useIconRender() {
interface IconConfig {
/**
* 图标名称(iconify图标的名称)
* - 例如:mdi-account 或者 mdi:account
*/
icon?: string
/** 图标颜色 */
color?: string
/** 图标大小 */
fontSize?: number
}
interface IconStyle {
color?: string
fontSize?: string
}
/**
* 图标渲染
* @param config
* @property icon - 图标名称(iconify图标的名称), 例如:mdi-account 或者 mdi:account
* @property localIcon - 本地svg图标文件名(assets/svg文件夹下)
* @property color - 图标颜色
* @property fontSize - 图标大小
*/
const iconRender = (config: IconConfig) => {
const { color, fontSize, icon } = config
const style: IconStyle = {}
if (color) {
style.color = color
}
if (fontSize) {
style.fontSize = `${fontSize}px`
}
if (!icon) {
window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!')
}
return () => h(SvgIcon, { icon, style })
}
return {
iconRender,
}
}
@@ -48,7 +48,7 @@ onMounted(() => {
</script>
<template>
<div class="company-detail-page">
<div class="detail-wrapper">
<div class="detail-container">
<div class="header-region">
<el-avatar shape="square" :size="64" class="logo" :src="companyDetail.logo || '/no-avatar.png'" />
@@ -150,7 +150,7 @@ onMounted(() => {
</template>
<style lang="scss" scoped>
.company-detail-page {
.detail-wrapper {
width: 100%;
display: flex;
justify-content: center;
@@ -1,12 +1,170 @@
<script lang="ts" setup>
import type { NavMenuItem } from '@/components/nav-menu/type'
import { Search } from '@element-plus/icons-vue'
import { computed, nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import NavMenu from '@/components/nav-menu/index.vue'
const searchKey = ref('')
const route = useRoute()
const menuRef = ref()
const menuList: NavMenuItem [] = [
{
label: '资料设置',
index: '/settings/info',
icon: 'hugeicons:setting-07',
children: [
{
label: '公司信息设置',
index: '/settings/info/company',
icon: 'hugeicons:building-02',
},
{
label: '成员设置',
index: '/settings/info/company-user',
icon: 'hugeicons:user-settings-01',
},
],
},
{
label: '创作者中心',
index: '/settings/forum',
icon: 'hugeicons:edit-01',
children: [
{
label: '首页',
index: '/settings/forum/home',
icon: 'hugeicons:home-03',
},
{
label: '写文章',
index: '/settings/forum/editor',
icon: 'hugeicons:edit-04',
},
{
label: '帖子管理',
index: '/settings/forum/post',
icon: 'hugeicons:material-and-texture',
},
],
},
]
/**
* 递归过滤菜单
*/
function filterMenu(
list: NavMenuItem[],
keyword: string,
): NavMenuItem[] {
if (!keyword)
return list
return list
.map((item) => {
const hitSelf = item.label.includes(keyword)
// 递归检查子节点是否命中(但不裁剪 children)
const hitChildren = item.children
? filterMenu(item.children, keyword).length > 0
: false
// 自己命中 或 任一子节点命中 → 保留整个节点
if (hitSelf || hitChildren) {
return item
}
return null
})
.filter(Boolean) as NavMenuItem[]
}
const filteredMenu = computed(() =>
filterMenu(menuList, searchKey.value.trim()),
)
function collectOpeneds(menu: NavMenuItem[], result: string[] = []) {
for (const item of menu) {
if (item.children?.length) {
result.push(item.index)
collectOpeneds(item.children, result)
}
}
return result
}
watch(searchKey, async (val) => {
await nextTick()
if (!menuRef.value)
return
if (val) {
const openeds = collectOpeneds(filteredMenu.value)
openeds.forEach((index) => {
menuRef.value!.open(index)
})
}
})
</script>
<template>
<div class="">
settings
<div class="settings-wrapper">
<div class="left-menu">
<el-input
v-model="searchKey"
placeholder="搜索菜单"
size="large"
:prefix-icon="Search"
clearable
/>
<el-menu
ref="menuRef"
router
:default-active="route.path"
>
<NavMenu :menu="filteredMenu" />
</el-menu>
</div>
<div class="right-view">
<router-view />
</div>
</div>
</template>
<style lang="scss" scoped>
//
.settings-wrapper {
display: flex;
gap: 40px;
margin-top: 30px;
padding: 0px 240px;
height: 800px;
.left-menu {
font-size: 16px;
overflow-y: auto;
width: 240px;
height: 100%;
background-color: #fff;
flex-shrink: 0;
:deep(.el-input__wrapper) {
margin: 12px 12px 4px 12px;
}
:deep(.el-menu) {
border-right: none;
}
}
.right-view {
width: 1200px;
flex-shrink: 0;
overflow-y: auto;
background-color: #fff;
border-radius: 4px;
}
}
</style>
@@ -0,0 +1,163 @@
<script lang="ts" setup>
import { reactive } from 'vue'
import WangEditor from '@/components/wang-editor/index.vue'
const form = reactive({
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
})
</script>
<template>
<div class="company-wrapper">
<div class="form-item">
<div class="form-header">
<span class="form-title">
基础信息
</span>
</div>
<el-form
:model="form"
label-width="auto"
label-position="top"
>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item>
<el-avatar :size="86" shape="square" />
<div class="logo-upload">
<el-button type="primary" plain>
上传LOGO
</el-button>
<span>支持 JPG、PNG 格式,文件大小不超过 2MB</span>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司名称">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属行业">
<el-select v-model="form.region" placeholder="please select your zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成立时间">
<el-date-picker
v-model="form.date1"
type="date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="员工规模">
<el-input v-model="form.desc" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司地址">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司详细地址">
<el-input v-model="form.desc" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系邮箱">
<el-input v-model="form.desc" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="form-item">
<div class="form-header">
<span class="form-title">
公司简介
</span>
</div>
<el-form :model="form" label-width="auto">
<el-form-item>
<el-input v-model="form.name" type="textarea" />
</el-form-item>
</el-form>
</div>
<div class="form-item">
<div class="form-header">
<span class="form-title">
公司介绍
</span>
</div>
<WangEditor />
</div>
<div class="form-footer">
<el-button>重置</el-button>
<el-button type="primary">
保存
</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.company-wrapper {
padding: 24px 32px;
.form-item {
margin-bottom: 16px;
.form-header {
margin-bottom: 16px;
.form-title {
font-size: 18px;
font-weight: 600;
}
}
.logo-upload {
margin-left: 16px;
display: flex;
flex-direction: column;
span {
font-size: 14px;
color: #666;
}
}
}
.form-footer {
width: 100%;
display: flex;
justify-content: flex-end;
:deep(.el-button){
width: 120px;
}
}
}
</style>
@@ -29,7 +29,11 @@ export default defineConfig((configEnv) => {
},
plugins: [
vue(),
Icons(),
Icons({
compiler: 'vue3',
scale: 1,
defaultClass: 'inline-block',
}),
Components({
dts: 'src/types/auto-import.d.ts',
resolvers: [IconsResolver()],