| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- <template>
- <div class="ele-body salary-field-page">
- <el-card shadow="never">
- <!-- 表头标题 -->
- <div class="salary-field-header">
- <div class="salary-field-title">
- <span class="title-bar"></span>
- <span class="title-text">薪资项目列表</span>
- </div>
- </div>
- <!-- 五个方向的归属说明(Y13:让 HR 知道每个方向谁在用、谁生成) -->
- <div class="direction-legend">
- <el-tooltip v-for="item in directionMeta" :key="item.value" placement="top" effect="light"
- :content="item.hint" :open-delay="200">
- <div class="direction-chip" :class="item.configurable ? 'is-configurable' : 'is-system'">
- <span class="chip-label">{{ item.label }}</span>
- <span class="chip-owner">{{ item.configurable ? '模板可配' : '系统生成' }}</span>
- </div>
- </el-tooltip>
- </div>
- <!-- 搜索表单:公司、项目编码、名称、方向、状态 -->
- <field-search @search="reload" />
- <!-- 数据表格 -->
- <ele-pro-table ref="table" :columns="columns" :datasource="datasource" :selection.sync="selection" row-key="id"
- :page-size="pageSize" @columns-change="handleColumnChange" :cache-key="cacheKeyUrl">
- <!-- 表头工具栏 -->
- <template v-slot:toolbar>
- <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openEdit()">
- 新增
- </el-button>
- <!-- C17:批量删除接口已就绪,勾选后出现批量删除入口 -->
- <el-button v-if="SALARY_ITEM_BATCH_DELETE_READY" type="danger" icon="el-icon-delete"
- class="ele-btn-icon" :disabled="!selection.length" @click="removeSelected">
- 批量删除{{ selection.length ? `(${selection.length})` : '' }}
- </el-button>
- </template>
- <!-- 是否启用列 -->
- <template v-slot:enabled="{ row }">
- <el-switch v-model="row.enabled" :active-value="true" :inactive-value="false"
- @change="(val) => handleToggleStatus(row, val)" />
- </template>
- <!-- 项目编码列:点击打开只读详情(列表统一约定 —— 编号列即详情入口) -->
- <template v-slot:itemCode="{ row }">
- <el-link v-if="row.itemCode" type="primary" :underline="false" @click="openView(row)">
- {{ row.itemCode }}
- </el-link>
- <span v-else>—</span>
- </template>
- <!-- 来源列 -->
- <template v-slot:source="{ row }">
- {{ formatSource(row) }}
- </template>
- <!-- 操作列 -->
- <template v-slot:action="{ row }">
- <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
- 编辑
- </el-link>
- <el-popconfirm class="ele-action" title="确定要删除当前薪资项目吗?删除后不可恢复" @confirm="remove(row)">
- <template v-slot:reference>
- <el-link type="danger" :underline="false" icon="el-icon-delete">
- 删除
- </el-link>
- </template>
- </el-popconfirm>
- </template>
- <template slot="empty">
- <el-empty description="暂无数据" />
- </template>
- </ele-pro-table>
- </el-card>
- <!-- 编辑 / 查看弹窗(同一组件,is-view 切换只读态) -->
- <field-edit :visible.sync="showEdit" :data="currentRow" :is-view="viewMode" @done="reload" />
- </div>
- </template>
- <script>
- import tabMixins from '@/mixins/tableColumnsMixin';
- import { getCurrentOrgId } from '@/utils/current-org';
- import FieldEdit from '../components/field-edit.vue';
- import FieldSearch from '../components/field-search.vue';
- import {
- SALARY_ITEM_TYPE_OPTIONS,
- SALARY_ITEM_DIRECTION_META,
- SALARY_ITEM_BATCH_DELETE_READY,
- salaryItemTypeLabel,
- getSalaryFieldPageList,
- deleteSalaryField,
- deleteSalaryFields,
- toggleSalaryFieldStatus
- } from '@/api/salary/fieldSetting';
- export default {
- name: 'SalaryFieldSetting',
- components: {
- FieldEdit,
- FieldSearch
- },
- mixins: [tabMixins],
- data() {
- return {
- searchParams: {},
- currentRow: null,
- showEdit: false,
- /* 弹框只读态:点「项目编码」查看时为 true,点「编辑」/「新增项目」时为 false */
- viewMode: false,
- pageSize: this.$store.state.tablePageSize || 20,
- cacheKeyUrl: 'salary-field-setting',
- directionMeta: SALARY_ITEM_DIRECTION_META,
- /* C17:批量删除接口就绪标记(模板里据此显示勾选列与批量删除按钮) */
- SALARY_ITEM_BATCH_DELETE_READY,
- columns: [
- /*
- * C17:批量删除接口 `POST /hr/payroll/items/batch-delete` 已就绪,
- * 多选列渲染出来,配合工具栏的「批量删除」。若后端回退该接口,
- * 只需把 SALARY_ITEM_BATCH_DELETE_READY 置 false,勾选列与按钮会一起消失。
- */
- ...(SALARY_ITEM_BATCH_DELETE_READY
- ? [
- {
- columnKey: 'selection',
- type: 'selection',
- width: 45,
- align: 'center',
- fixed: 'left'
- }
- ]
- : []),
- {
- label: '序号',
- type: 'index',
- width: 55,
- align: 'center'
- },
- {
- prop: 'itemCode',
- label: '项目编码',
- showOverflowTooltip: true,
- align: 'center',
- minWidth: 130,
- /* 点编码开只读详情(替代操作列「查看」入口) */
- slot: 'itemCode'
- },
- {
- prop: 'fieldName',
- label: '项目名称',
- showOverflowTooltip: true,
- align: 'center',
- minWidth: 140
- },
- {
- prop: 'itemType',
- label: '项目方向',
- width: 100,
- align: 'center',
- formatter: (row) => this.formatItemType(row)
- },
- {
- prop: 'fieldHint',
- label: '项目备注',
- showOverflowTooltip: true,
- align: 'center',
- minWidth: 160
- },
- {
- columnKey: 'enabled',
- label: '是否启用',
- width: 110,
- align: 'center',
- slot: 'enabled'
- },
- {
- columnKey: 'source',
- label: '来源',
- width: 130,
- align: 'center',
- slot: 'source'
- },
- {
- prop: 'createUserName',
- label: '创建人',
- showOverflowTooltip: true,
- align: 'center',
- minWidth: 140
- },
- {
- prop: 'createTime',
- label: '创建时间',
- align: 'left',
- minWidth: 90
- },
- {
- prop: 'updateUserName',
- label: '修改人',
- showOverflowTooltip: true,
- align: 'center',
- minWidth: 140
- },
- {
- prop: 'updateTime',
- label: '修改时间',
- align: 'left',
- minWidth: 90
- },
- {
- columnKey: 'action',
- label: '操作',
- width: 160,
- align: 'center',
- resizable: false,
- slot: 'action',
- showOverflowTooltip: true
- }
- ],
- selection: []
- };
- },
- methods: {
- /* 表格数据源:合并搜索表单条件,保证首屏请求也带着默认所属公司 */
- async datasource({ page, limit, where, order }) {
- const params = { ...this.searchParams, ...where, ...order };
- /*
- * F11:companyId 兜底。首屏请求有可能早于搜索表单的首次 emit(组件挂载顺序),
- * 此时 searchParams 里还没有 companyId,接口会因「查询薪资项目必须指定所属机构」直接失败。
- * 只在**从未设置过**(undefined)时兜底;用户主动清空('' = 查询全部授权法人)时保持原样 ——
- * 那是一个有意义的筛选值,不能被兜底覆盖掉。
- */
- if (params.companyId === undefined) {
- params.companyId = getCurrentOrgId() || '';
- }
- return await getSalaryFieldPageList({
- ...params,
- pageNum: page,
- size: limit
- });
- },
- /* 刷新表格 */
- reload(where) {
- // 搜索表单在 created 阶段就会带默认所属公司 emit 一次,此时表格实例还没挂载
- // ($refs.table 为空),这里先只记下条件,交给表格首次加载读 searchParams,
- // 否则首屏请求会漏掉 companyId
- if (where) this.searchParams = where;
- if (this.$refs.table) {
- this.$refs.table.reload({ page: 1, where: this.searchParams });
- }
- },
- /* 打开编辑弹窗(新增 / 编辑,均为可写态) */
- openEdit(row) {
- this.viewMode = false;
- this.currentRow = row || null;
- this.showEdit = true;
- },
- /*
- * 打开只读详情(点「项目编码」触发)。
- * 直接复用 field-edit 的 isView 只读态:表单整体 disabled、隐藏「套用推荐属性」、
- * 底部只留「关闭」,不新增第二个弹框组件。
- * 行数据来自列表接口,含 itemType / dataType / 四个属性 / sourceType,足够回显,无需再取详情。
- */
- openView(row) {
- this.viewMode = true;
- this.currentRow = row || null;
- this.showEdit = true;
- },
- /* 来源格式化(后端枚举 → 业务文案;兼容后端直接回中文的旧值) */
- formatSource(row) {
- const source = String(row.source || '').trim();
- const key = source.toUpperCase();
- if (key === 'MANUAL' || source === '手动添加' || source === '手工添加') {
- return '手动添加';
- }
- if (key === 'SYSTEM' || source === '系统内置') return '系统内置';
- if (key === 'IMPORT' || source === '导入') return '导入';
- if (key === 'API' || source === '接口' || source === '接口同步') {
- return '系统生成';
- }
- return source || '手动添加';
- },
- /* 项目方向格式化(枚举与后端 itemType 一致,未识别取值原样显示) */
- formatItemType(row) {
- return salaryItemTypeLabel(row.itemType);
- },
- /* 启用/停用 */
- async handleToggleStatus(row, val) {
- try {
- await toggleSalaryFieldStatus(row.id, val);
- this.$message.success(val ? '已启用' : '已停用');
- } catch (error) {
- // 还原开关状态
- row.enabled = !val;
- this.$message.error(error.message || '操作失败');
- }
- },
- /* 删除(单条)。
- P2-22 ②:批量删除入口等后端 `POST /hr/payroll/items/batch-delete`(需求 C17)。
- API 层已经不再吞掉失败原因 —— 删除失败会抛带明细的错误,这里原样展示(长文案加长驻留)。*/
- async remove(row) {
- const loading = this.$loading({ lock: true });
- try {
- const msg = await deleteSalaryField([row.id]);
- loading.close();
- this.$message.success('删除' + (msg || '成功'));
- this.reload();
- } catch (error) {
- loading.close();
- this.$message({
- type: 'error',
- message: error.message || '删除失败',
- showClose: true,
- duration: 8000
- });
- }
- },
- /**
- * C17:批量删除(勾选行)。
- * 走 `POST /hr/payroll/items/batch-delete`(单事务,最多 200/次,超出自动分片)。
- * 契约会给每个失败项稳定错误码,这里把「成功几条 / 失败几条 + 逐条原因」完整摆出来,
- * 不再出现「删了一半还提示成功」。
- */
- async removeSelected() {
- const rows = this.selection || [];
- if (!rows.length) {
- this.$message.warning('请先勾选要删除的薪资项目');
- return;
- }
- const reasons = rows
- .slice(0, 8)
- .map((row) => `${row.itemCode || '-'} ${row.fieldName || ''}`.trim())
- .join('、');
- try {
- await this.$confirm(
- `确定删除已勾选的 ${rows.length} 个薪资项目吗?删除后不可恢复。\n${reasons}${
- rows.length > 8 ? ' 等' : ''
- }`,
- '批量删除确认',
- { type: 'warning', confirmButtonText: '确定删除', cancelButtonText: '取消' }
- );
- } catch (error) {
- return;
- }
- const loading = this.$loading({ lock: true });
- try {
- const result = await deleteSalaryFields(rows.map((row) => row.id));
- loading.close();
- if (result.allSucceeded) {
- this.$message.success(`已删除 ${result.successIds.length} 个项目`);
- } else {
- const detail = result.failed
- .slice(0, 5)
- .map((item) => `#${item.id}:${item.reasonText || item.reason}`)
- .join(';');
- this.$message({
- type: 'warning',
- showClose: true,
- duration: 8000,
- message: `成功 ${result.successIds.length} 个,失败 ${result.failed.length} 个(${detail}${
- result.failed.length > 5 ? ' 等' : ''
- })`
- });
- }
- this.selection = [];
- this.reload();
- } catch (error) {
- loading.close();
- this.$message({
- type: 'error',
- message: error.message || '批量删除失败',
- showClose: true,
- duration: 8000
- });
- }
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .salary-field-page {
- .salary-field-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
- }
- .salary-field-title {
- display: flex;
- align-items: center;
- .title-bar {
- display: inline-block;
- width: 3px;
- height: 16px;
- background: var(--color-primary, #1890ff);
- border-radius: 2px;
- margin-right: 8px;
- }
- .title-text {
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- }
- /* 五个方向的归属图例 */
- .direction-legend {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- margin-bottom: 14px;
- }
- .direction-chip {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 10px;
- border-radius: 4px;
- font-size: 13px;
- line-height: 20px;
- cursor: default;
- &.is-configurable {
- background: #ecf5ff;
- border: 1px solid #b3d8ff;
- .chip-label {
- color: #1559c8;
- font-weight: 600;
- }
- .chip-owner {
- color: #409eff;
- }
- }
- &.is-system {
- background: #f4f4f5;
- border: 1px solid #e9e9eb;
- .chip-label {
- color: #606266;
- font-weight: 600;
- }
- .chip-owner {
- color: #909399;
- }
- }
- }
- }
- </style>
|