feat(frontend): 菜单图标与视觉设计系统落地
- 引入 @element-plus/icons-vue 官方图标库,菜单项按模块配图标,消除 纯文字导航的辨识成本(新增依赖已评估:Element Plus 官方同源维护) - AppMenu/BasicLayout/router 同步:图标渲染、菜单结构与路由 meta 对齐 - index.css 视觉系统化:品牌色、语义三色(ok/warn/danger 及其 wash 层次)、 形体与材质(圆角、缓动、阴影、等宽字体)统一为 CSS 变量,并覆盖 Element Plus 主色 - 登录页、业务页、结算页、工作台、审批页等视图随新变量适配 验证:pnpm type-check 与 pnpm build 均通过;后端接口契约未变。 Co-Authored-By: Claude Code <noreply@anthropic.com>
这个提交包含在:
1 父节点
f88857e026
当前提交
e61800dcdc
34 个文件已更改
+3960
-2041
没有与搜索条件相匹配的文件
@@ -8,6 +8,7 @@ import type { PersonnelItem } from '@/api/personnel'
|
||||
import AppPagination from '@/components/table/AppPagination.vue'
|
||||
import StatusTag from '@/components/table/StatusTag.vue'
|
||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||
import { useDropdownOptions } from '@/composables/useDropdownOptions'
|
||||
import { usePagedList } from '@/composables/usePagedList'
|
||||
import { useEditingRow } from '@/composables/useEditingRow'
|
||||
import { AMOUNT_PATTERN } from '@/utils/validate'
|
||||
@@ -64,29 +65,22 @@ interface SelectOption {
|
||||
status: number
|
||||
}
|
||||
|
||||
const roleOptions = ref<SelectOption[]>([])
|
||||
const managerOptions = ref<SelectOption[]>([])
|
||||
|
||||
async function loadRoleOptions(): Promise<void> {
|
||||
try {
|
||||
const { options: roleOptions, load: loadRoleOptions } = useDropdownOptions<SelectOption>(
|
||||
async () => {
|
||||
// 岗位规模有限(内置 3 个 + 自定义),选项接口一次取全
|
||||
const data = await businessRoleApi.listBusinessRoleOptions()
|
||||
roleOptions.value = data.map((r) => ({ id: r.id, name: r.role_name, status: r.status }))
|
||||
} catch (error) {
|
||||
handledError(error) // 下拉为空不阻断列表使用
|
||||
}
|
||||
}
|
||||
return data.map((r) => ({ id: r.id, name: r.role_name, status: r.status }))
|
||||
},
|
||||
)
|
||||
|
||||
async function loadManagerOptions(): Promise<void> {
|
||||
try {
|
||||
const { options: managerOptions, load: loadManagerOptions } = useDropdownOptions<SelectOption>(
|
||||
async () => {
|
||||
// 经理下拉的近似全量数据源:内部系统人员规模有限,200 条足够;
|
||||
// 超出规模的极端场景由后端对经理合法性的校验兜底
|
||||
const data = await personnelApi.listPersonnel({ pageNum: 1, pageSize: 200 })
|
||||
managerOptions.value = data.list.map((p) => ({ id: p.id, name: p.personnel_name, status: p.status }))
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
}
|
||||
}
|
||||
return data.list.map((p) => ({ id: p.id, name: p.personnel_name, status: p.status }))
|
||||
},
|
||||
)
|
||||
|
||||
// ==================== 新增 / 编辑(基本信息) ====================
|
||||
|
||||
@@ -372,54 +366,73 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<el-form class="filter-form" :inline="true" @submit.prevent>
|
||||
<el-form-item label="姓名">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="姓名,模糊匹配"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="search"
|
||||
<div class="page-head">
|
||||
<h1 class="page-title">人员管理</h1>
|
||||
<p class="page-desc">
|
||||
人员岗位与适用计提参数在这里定;改岗位会改变后续回款的绩效口径,设为离职后不可再被业务与客户归属引用
|
||||
</p>
|
||||
<span class="waterline"></span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
.filter-bar 直接挂在 el-form 上:el-form 不是 flex 透明容器,
|
||||
外面再包一层 div 会让 el-form-item 丢掉 gap 排版,筛选条退化成竖排。
|
||||
-->
|
||||
<el-form class="filter-bar" @submit.prevent>
|
||||
<el-form-item label="姓名">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="姓名,模糊匹配"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 110px">
|
||||
<el-option
|
||||
v-for="item in EMPLOYMENT_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 110px">
|
||||
<el-option
|
||||
v-for="item in EMPLOYMENT_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位">
|
||||
<el-select
|
||||
v-model="filters.business_role_id"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in searchRoleOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openCreate">新增人员</el-button>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位">
|
||||
<el-select
|
||||
v-model="filters.business_role_id"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in searchRoleOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="filter-bar__actions">
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="records" stripe>
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openCreate">新增人员</el-button>
|
||||
<span class="toolbar-note">辐射区域与拓展计划在行内「区域与计划」维护,保存即全量替换</span>
|
||||
</div>
|
||||
|
||||
<!-- ys-rise:整页只给列表卡一次入场上浮,筛选与页头立即呈现,避免整屏一起动 -->
|
||||
<div class="card ys-rise">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="records"
|
||||
stripe
|
||||
empty-text="还没有人员 —— 点「新增人员」建第一个,或换个关键词再查"
|
||||
>
|
||||
<el-table-column prop="personnel_name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="role_name" label="岗位" min-width="100">
|
||||
<template #default="{ row }">{{ row.role_name ?? '—' }}</template>
|
||||
@@ -444,7 +457,7 @@ onMounted(() => {
|
||||
<el-table-column label="工资标准(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 字符串直出:金额红线,禁 toFixed / float 格式化 -->
|
||||
<span class="cell-amount">{{ row.salary_standard ?? '—' }}</span>
|
||||
<span class="cell-num">{{ row.salary_standard ?? '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
@@ -469,14 +482,16 @@ onMounted(() => {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AppPagination
|
||||
:total="total"
|
||||
:page-num="pageNum"
|
||||
:page-size="pageSize"
|
||||
@page-change="onPageChange"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
<div class="card-foot">
|
||||
<AppPagination
|
||||
:total="total"
|
||||
:page-num="pageNum"
|
||||
:page-size="pageSize"
|
||||
@page-change="onPageChange"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增 / 编辑(基本信息;不含状态与子表) -->
|
||||
<el-dialog
|
||||
@@ -567,7 +582,12 @@ onMounted(() => {
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="regionRows" stripe size="small">
|
||||
<el-table
|
||||
:data="regionRows"
|
||||
stripe
|
||||
size="small"
|
||||
empty-text="还没有辐射区域 —— 点「添加区域」加一条"
|
||||
>
|
||||
<el-table-column label="区域名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.region_name" maxlength="128" placeholder="如 沈阳市铁西区" />
|
||||
@@ -598,7 +618,12 @@ onMounted(() => {
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="planRows" stripe size="small">
|
||||
<el-table
|
||||
:data="planRows"
|
||||
stripe
|
||||
size="small"
|
||||
empty-text="还没有拓展计划 —— 点「添加计划」加一条"
|
||||
>
|
||||
<el-table-column label="期间" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.plan_period" maxlength="32" placeholder="如 2026Q1" />
|
||||
@@ -629,23 +654,19 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cell-muted {
|
||||
color: var(--ys-ink-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cell-amount {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--ys-ink-2);
|
||||
}
|
||||
/* .cell-muted / .cell-num 由全局 index.css 提供,本页不再重复定义 */
|
||||
|
||||
/*
|
||||
* 试用期的计算口径必须写在开关旁:它不是状态而是计提规则,
|
||||
* 单靠「试用」二字看不出「照算不照发、超 18000 年底结」。
|
||||
*/
|
||||
.switch-hint {
|
||||
margin-left: 10px;
|
||||
color: var(--ys-ink-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 抽屉内的子表分组:区域与计划各自成块,块内标题与操作按钮同行 */
|
||||
.sub-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户