You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

682 lines
22 KiB

<template>
<div class="app-container points-management">
<el-tabs v-model="activeTab">
<el-tab-pane label="积分规则" name="rules">
<data-list-panel modern-table>
<template #filters>
<el-form
ref="queryForm"
:model="queryParams"
size="small"
:inline="true"
class="data-list-filter-form"
>
<el-form-item v-if="isSuperAdmin" label="厂区" prop="deptId">
<el-select
v-model="queryParams.deptId"
clearable
filterable
placeholder="全部厂区"
@change="handleQuery"
>
<el-option
v-for="item in factoryOptions"
:key="String(item.deptId || item.id)"
:label="item.deptName || item.label || item.name"
:value="String(item.deptId || item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="隐患等级" prop="hazardLevel">
<el-select
v-model="queryParams.hazardLevel"
clearable
placeholder="全部等级"
@change="handleQuery"
>
<el-option
v-for="item in levelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="enabled">
<el-select
v-model="queryParams.enabled"
clearable
placeholder="全部状态"
@change="handleQuery"
>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
@click="handleQuery"
>查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</template>
<template #actions>
<div class="data-list-actions">
<el-button
type="primary"
size="small"
icon="el-icon-plus"
@click="openAdd"
>新建</el-button>
</div>
</template>
<template #header>
<div class="rule-heading">
<div class="rule-heading__title">积分规则配置 · 多档位</div>
<div class="rule-heading__description">
同一隐患等级支持 A/B/C 多档积分标准,档位由后端规则引擎按“等级 × 档位”匹配。
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="ruleList"
border
class="data-list-table"
empty-text="暂无积分规则"
>
<el-table-column label="隐患等级" min-width="180">
<template slot-scope="scope">
<el-tag :type="levelTagType(scope.row)" size="small">
{{ levelLabel(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="厂区"
min-width="160"
show-overflow-tooltip
>
<template slot-scope="scope">
{{ factoryLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column label="档位" width="120" align="center">
<template slot-scope="scope">
<span class="tier-text">{{ tierLabel(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column prop="points" label="积分" width="120" align="center" />
<el-table-column label="启用" width="120" align="center">
<template slot-scope="scope">
<el-switch
:value="toEnabled(scope.row.enabled)"
:active-value="1"
:inactive-value="0"
@change="toggleEnabled(scope.row, $event)"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
width="160"
align="center"
fixed="right"
class-name="data-list-operation-column"
>
<template slot-scope="scope">
<el-button size="small" @click="openDetail(scope.row)">详情</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<div class="rule-version">
<div class="rule-version__actions">
<el-button
type="primary"
size="small"
@click="showPendingAction('保存为新版本 V4')"
>保存为新版本 V4</el-button>
<el-button
size="small"
@click="showPendingAction('历史月份试算')"
>历史月份试算</el-button>
</div>
<div class="rule-version__note">
档位标准业主正在内部商讨,以上为演示配置;档位匹配逻辑由后端规则引擎判定(等级 × 档位 → 积分)。
</div>
</div>
</template>
</data-list-panel>
</el-tab-pane>
><el-tab-pane label="申诉处理" name="appeal">
<appeal-ledger-panel v-if="activeTab === 'appeal'" />
</el-tab-pane><el-tab-pane
label="手动调整"
name="adjust"
disabled /><el-tab-pane label="流水冲正" name="reverse">
<transaction-reverse-panel v-if="activeTab === 'reverse'" />
</el-tab-pane></el-tabs
><el-dialog
title="积分规则详情"
:visible.sync="detailVisible"
width="560px"
append-to-body
><el-descriptions v-if="detail" :column="2" border size="small"
><el-descriptions-item label="厂区">{{
factoryLabel(detail)
}}</el-descriptions-item
><el-descriptions-item label="隐患等级">{{
levelLabel(detail)
}}</el-descriptions-item
><el-descriptions-item label="档位">{{
tierLabel(detail)
}}</el-descriptions-item
><el-descriptions-item label="积分">{{
detail.points == null ? "—" : detail.points
}}</el-descriptions-item
><el-descriptions-item label="启用状态">{{
toEnabled(detail.enabled) ? "启用" : "停用"
}}</el-descriptions-item
><el-descriptions-item label="优先级">{{
detail.priority || "—"
}}</el-descriptions-item
><el-descriptions-item label="规则编码">{{
detail.ruleCode || "—"
}}</el-descriptions-item
><el-descriptions-item label="规则名称">{{
detail.ruleName || "—"
}}</el-descriptions-item
><el-descriptions-item label="备注" :span="2">{{
detail.remark || "—"
}}</el-descriptions-item
><el-descriptions-item label="创建时间">{{
formatTime(detail.createTime)
}}</el-descriptions-item
><el-descriptions-item label="更新时间">{{
formatTime(detail.updateTime)
}}</el-descriptions-item></el-descriptions
><el-empty v-else description="暂无详情" /></el-dialog
><el-dialog
title="新建积分规则"
:visible.sync="addVisible"
width="560px"
append-to-body
><el-form
ref="addForm"
:model="addForm"
:rules="addRules"
label-width="92px"
size="small"
><el-form-item label="所属厂区" prop="deptId"
><el-select
v-model="addForm.deptId"
:disabled="!isSuperAdmin"
:loading="factoryLoading"
filterable
placeholder="请选择厂区"
style="width: 100%"
><el-option
v-for="item in displayedFactoryOptions"
:key="factoryId(item)"
:label="factoryName(item)"
:value="factoryId(item)" /></el-select></el-form-item
><el-form-item label="隐患等级" prop="hazardLevel"
><el-select
v-model="addForm.hazardLevel"
placeholder="请选择隐患等级"
style="width: 100%"
@change="suggestTier"
><el-option
v-for="item in levelOptions"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select></el-form-item
><el-form-item label="档位" prop="hazardCategory"
><el-input
v-model.trim="addForm.hazardCategory"
maxlength="4"
placeholder="如 A、B、C"
@input="normalizeTier" /></el-form-item
><el-form-item label="积分" prop="points"
><el-input-number
v-model="addForm.points"
:min="0"
:precision="0"
style="width: 100%" /></el-form-item
><el-form-item label="优先级"
><el-input-number
v-model="addForm.priority"
:min="1"
:precision="0"
style="width: 100%" /></el-form-item
><el-form-item label="规则编码"
><el-input v-model.trim="addForm.ruleCode" /></el-form-item
><el-form-item
label="规则名称"
prop="ruleName"
><el-input v-model.trim="addForm.ruleName" /></el-form-item
><el-form-item label="备注"
><el-input
v-model.trim="addForm.remark"
type="textarea"
:rows="2" /></el-form-item></el-form
><span slot="footer"
><el-button @click="addVisible = false">取消</el-button
><el-button
type="primary"
:loading="saving"
:disabled="factoryLoading || !canSubmitAdd"
@click="submitAdd"
>确定</el-button
></span
></el-dialog
>
</div>
</template>
<script>
import {
listPointsRules,
togglePointsRule,
deletePointsRule,
getPointsRule,
addPointsRule,
} from "@/api/points/rule";
import {
listFactoryDepartments,
getMyFactoryDepartment,
} from "@/api/system/dept";
import TransactionReversePanel from "./components/TransactionReversePanel";
import AppealLedgerPanel from "@/views/points/appeal/components/AppealLedgerPanel";
const LEVELS = [
{ value: "general", label: "一般隐患" },
{ value: "major", label: "较大隐患" },
{ value: "critical", label: "重大隐患" },
];
export default {
name: "PointsManagement",
components: { TransactionReversePanel, AppealLedgerPanel },
data() {
const deptRequired = (rule, value, callback) => {
if (!value) callback(new Error("请选择所属厂区"));
else callback();
};
return {
activeTab: "rules",
loading: false,
factoryLoading: false,
saving: false,
total: 0,
ruleList: [],
factoryOptions: [],
currentFactory: null,
detail: null,
detailVisible: false,
addVisible: false,
levelOptions: LEVELS,
queryParams: {
pageNum: 1,
pageSize: 10,
deptId: undefined,
hazardLevel: undefined,
enabled: undefined,
},
addForm: {
deptId: "",
hazardLevel: "",
hazardCategory: "",
points: null,
priority: 1,
ruleCode: "",
ruleName: "",
remark: "",
},
addRules: {
deptId: [{ validator: deptRequired, trigger: "change" }],
hazardLevel: [
{ required: true, message: "请选择隐患等级", trigger: "change" },
],
hazardCategory: [
{ required: true, message: "请输入档位", trigger: "blur" },
{
pattern: /^[A-Z]+$/,
message: "档位请输入大写英文字母",
trigger: "blur",
},
],
points: [{ required: true, message: "请输入积分", trigger: "change" }],
ruleName: [
{ required: true, message: '请输入规则名称', trigger: 'blur' }
]
},
};
},
computed: {
isSuperAdmin() {
return (this.$store.getters.roles || []).indexOf("admin") !== -1;
},
displayedFactoryOptions() {
if (this.isSuperAdmin) return this.factoryOptions;
return this.currentFactory ? [this.currentFactory] : [];
},
canSubmitAdd() {
return this.isSuperAdmin || Boolean(this.factoryId(this.currentFactory));
},
},
created() {
this.getList();
this.loadFactoryScope();
},
methods: {
getList() {
this.loading = true;
const params = Object.assign({}, this.queryParams);
if (!this.isSuperAdmin) delete params.deptId;
listPointsRules(params)
.then((res) => {
const page = res.data || res;
this.ruleList = this.sortRules(page.list || page.rows || []);
this.total = Number(
page.total || page.totalCount || this.ruleList.length
);
})
.catch(() => {
this.ruleList = [];
this.total = 0;
this.$modal.msgError("积分规则加载失败");
})
.finally(() => {
this.loading = false;
});
},
loadFactoryScope() {
this.factoryLoading = true;
const request = this.isSuperAdmin
? listFactoryDepartments()
: getMyFactoryDepartment();
request
.then((res) => {
const data = res.data || res;
if (this.isSuperAdmin) {
this.factoryOptions = Array.isArray(data)
? data
: data.list || data.rows || [];
return;
}
this.currentFactory = data || null;
if (this.addVisible) {
this.addForm.deptId = this.factoryId(this.currentFactory);
}
})
.catch(() => {
this.factoryOptions = [];
this.currentFactory = null;
this.$modal.msgError("厂区信息加载失败");
})
.finally(() => {
this.factoryLoading = false;
});
},
sortRules(rows) {
const order = { general: 0, major: 1, critical: 2 };
return rows
.slice()
.sort(
(a, b) =>
(order[String(a.hazardLevel).toLowerCase()] == null
? 99
: order[String(a.hazardLevel).toLowerCase()]) -
(order[String(b.hazardLevel).toLowerCase()] == null
? 99
: order[String(b.hazardLevel).toLowerCase()]) ||
this.tierLabel(a).localeCompare(this.tierLabel(b)) ||
Number(b.priority || 0) - Number(a.priority || 0)
);
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.$refs.queryForm.resetFields();
this.handleQuery();
},
openAdd() {
this.addForm = {
deptId: this.isSuperAdmin ? "" : this.factoryId(this.currentFactory),
hazardLevel: "",
hazardCategory: "",
points: null,
priority: 1,
ruleCode: "",
ruleName: "",
remark: "",
};
this.addVisible = true;
this.$nextTick(() => this.$refs.addForm.clearValidate());
},
suggestTier() {
this.addForm.hazardCategory = this.nextTier(this.addForm.hazardLevel);
this.addForm.priority = this.nextPriority(this.addForm.hazardLevel);
},
nextPriority(level) {
const values = this.ruleList
.filter(
(row) =>
String(row.hazardLevel).toLowerCase() ===
String(level).toLowerCase()
)
.map((row) => Number(row.priority) || 0);
return (values.length ? Math.max.apply(null, values) : 0) + 1;
},
nextTier(level) {
const letters = this.ruleList
.filter(
(row) =>
String(row.hazardLevel).toLowerCase() ===
String(level).toLowerCase()
)
.map(this.tierLabel);
let max = 0;
letters.forEach((value) => {
let n = 0;
for (let i = 0; i < value.length; i++)
n = n * 26 + value.charCodeAt(i) - 64;
max = Math.max(max, n);
});
return this.numberToLetters(max + 1);
},
numberToLetters(number) {
let result = "";
while (number > 0) {
result = String.fromCharCode(65 + ((number - 1) % 26)) + result;
number = Math.floor((number - 1) / 26);
}
return result || "A";
},
normalizeTier(value) {
this.addForm.hazardCategory = String(value || "")
.replace(/[^a-z]/gi, "")
.toUpperCase();
},
submitAdd() {
if (!this.canSubmitAdd) {
this.$modal.msgError("当前厂区信息未加载,暂不能新增积分规则");
return;
}
this.$refs.addForm.validate((valid) => {
if (!valid) return;
const payload = {
hazardLevel: this.addForm.hazardLevel,
hazardCategory: this.addForm.hazardCategory,
points: this.addForm.points,
enabled: 1,
priority: this.addForm.priority,
ruleCode: this.addForm.ruleCode || undefined,
ruleName: this.addForm.ruleName,
remark: this.addForm.remark || undefined,
};
if (this.isSuperAdmin) {
payload.deptId = this.addForm.deptId;
const item = this.factoryOptions.find(
(row) => this.factoryId(row) === String(payload.deptId)
);
if (item) payload.deptName = this.factoryName(item);
}
this.saving = true;
addPointsRule(payload)
.then(() => {
this.$modal.msgSuccess("新增积分规则成功");
this.addVisible = false;
this.getList();
})
.finally(() => {
this.saving = false;
});
});
},
toggleEnabled(row, value) {
const old = this.toEnabled(row.enabled);
togglePointsRule(row.id, Number(value))
.then(() => {
row.enabled = Number(value);
this.$modal.msgSuccess("操作成功");
})
.catch(() => {
row.enabled = old;
});
},
handleDelete(row) {
this.$confirm("确认删除该积分规则吗?删除后不可恢复。", "删除确认", {
type: "warning",
})
.then(() => deletePointsRule(row.id))
.then(() => {
this.$modal.msgSuccess("删除成功");
this.getList();
});
},
openDetail(row) {
this.detailVisible = true;
this.detail = null;
getPointsRule(row.id).then((res) => {
this.detail = res.data || res;
});
},
showPendingAction(name) {
this.$modal.msgInfo(`${name}功能暂未接入接口`);
},
factoryId(item) {
const value =
item && item.deptId !== undefined ? item.deptId : item && item.id;
return value === undefined || value === null ? "" : String(value);
},
factoryName(item) {
return (item && (item.deptName || item.label || item.name)) || "";
},
factoryLabel(row) {
return this.factoryName(row) || this.factoryId(row) || "全局规则";
},
toEnabled(value) {
return Number(value) === 1 ? 1 : 0;
},
tierLabel(row) {
return String(row.hazardCategory || "—").toUpperCase();
},
levelLabel(row) {
return (
row.hazardLevelName ||
(
LEVELS.find(
(item) => item.value === String(row.hazardLevel).toLowerCase()
) || {}
).label ||
row.hazardLevel ||
"—"
);
},
levelTagType(row) {
return String(row.hazardLevel).toLowerCase() === "critical"
? "danger"
: String(row.hazardLevel).toLowerCase() === "major"
? "warning"
: "info";
},
formatTime(value) {
return value ? String(value).replace("T", " ").slice(0, 19) : "—";
},
},
};
</script>
<style lang="scss" scoped>
.points-management {
min-height: calc(100vh - 52px);
background: #f0f2f5;
}
.rule-heading {
min-width: 0;
}
.rule-heading__title {
color: #1a1a2e;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.rule-heading__description {
margin-top: 6px;
color: #9898ae;
font-size: 11px;
font-weight: 400;
line-height: 18px;
}
.tier-text {
display: inline-flex;
padding: 1px 8px;
color: #3b5fd9;
background: #eef1fe;
border: 1px solid #c7d2f8;
border-radius: 8px;
font-size: 11px;
line-height: 18px;
font-weight: 500;
}
.rule-version {
margin-top: 12px;
padding-top: 16px;
border-top: 1px solid #f0f0f2;
}
.rule-version__actions {
display: flex;
align-items: center;
gap: 8px;
}
.rule-version__actions .el-button + .el-button {
margin-left: 0;
}
.rule-version__note {
margin-top: 10px;
color: #9898ae;
font-size: 11px;
line-height: 18px;
}
@media (max-width: 800px) {
.rule-version__actions {
flex-wrap: wrap;
}
}
</style>