Merge branch 'main' of http://git.dibox.tech/GY/smart_grid_iot
This commit is contained in:
commit
737244fa9e
36 files changed
+3309
-246
No files matched your search
@@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ReceiverConfig } from './noticeRule';
|
||||
|
||||
export interface NoticeBatch {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
receiverConfig?: ReceiverConfig;
|
||||
receiverCount: number;
|
||||
sentBy?: number | null;
|
||||
sentAt?: string | null;
|
||||
sentByName?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface NoticeBatchQuery {
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface NoticeBatchListResult {
|
||||
list: NoticeBatch[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface NoticeBatchSendPayload {
|
||||
title: string;
|
||||
content: string;
|
||||
receiverConfig: ReceiverConfig;
|
||||
}
|
||||
|
||||
export interface NoticeBatchSendResult {
|
||||
batchId: number;
|
||||
receiverCount: number;
|
||||
}
|
||||
|
||||
// 手动群发(发送记录 + 立即发送)
|
||||
export const noticeBatchApi = {
|
||||
getList: (params: NoticeBatchQuery) =>
|
||||
request.get<NoticeBatchListResult>('/notice-batches', { params }),
|
||||
|
||||
send: (data: NoticeBatchSendPayload) =>
|
||||
request.post<NoticeBatchSendResult>('/notice-batches', data),
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 接收对象-管理端用户:按角色编码 + 按用户 id
|
||||
export interface ReceiverUsers {
|
||||
roles: string[];
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
// 接收对象-电工:none 不发 / event 事件关联电工 / all 全部在岗 / ids 指定 / status 按状态
|
||||
export type ReceiverWorkerMode = 'none' | 'event' | 'all' | 'ids' | 'status';
|
||||
|
||||
export interface ReceiverWorkers {
|
||||
mode: ReceiverWorkerMode;
|
||||
ids: number[];
|
||||
// 审核状态:0待审核 1已通过 2已驳回
|
||||
auditStatus?: number;
|
||||
// 在岗状态:1在岗 0离职
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface ReceiverConfig {
|
||||
users: ReceiverUsers;
|
||||
workers: ReceiverWorkers;
|
||||
}
|
||||
|
||||
/** 生成默认接收对象配置(两端共用) */
|
||||
export function createDefaultReceiverConfig(): ReceiverConfig {
|
||||
return {
|
||||
users: { roles: [], ids: [] },
|
||||
workers: { mode: 'none', ids: [] },
|
||||
};
|
||||
}
|
||||
|
||||
export interface NoticeRule {
|
||||
id: number;
|
||||
ruleKey: string;
|
||||
ruleName: string;
|
||||
eventKey: string;
|
||||
// 1扫描式 2埋点式
|
||||
triggerMode: number;
|
||||
// 1启用 0停用
|
||||
enabled: number;
|
||||
advanceDays?: number | null;
|
||||
titleTpl: string;
|
||||
contentTpl: string;
|
||||
receiverConfig: ReceiverConfig;
|
||||
remark?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/** 模板可用变量 */
|
||||
export interface NoticeEventVariable {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** 通知事件元数据 */
|
||||
export interface NoticeEventMeta {
|
||||
eventKey: string;
|
||||
eventName: string;
|
||||
// 1扫描式 2埋点式
|
||||
triggerMode: number;
|
||||
triggerModeText: string;
|
||||
variables: NoticeEventVariable[];
|
||||
}
|
||||
|
||||
export interface NoticeRuleQuery {
|
||||
keyword?: string;
|
||||
eventKey?: string;
|
||||
triggerMode?: number;
|
||||
enabled?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface NoticeRuleListResult {
|
||||
list: NoticeRule[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 新增/更新通知规则入参(更新时不传 ruleKey) */
|
||||
export interface NoticeRulePayload {
|
||||
ruleKey?: string;
|
||||
ruleName: string;
|
||||
eventKey: string;
|
||||
triggerMode: number;
|
||||
enabled: number;
|
||||
advanceDays?: number;
|
||||
titleTpl: string;
|
||||
contentTpl: string;
|
||||
receiverConfig: ReceiverConfig;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface NoticeScanResult {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
// 通知规则(规则配置 + 立即扫描)
|
||||
export const noticeRuleApi = {
|
||||
getList: (params: NoticeRuleQuery) => request.get<NoticeRuleListResult>('/notice-rules', { params }),
|
||||
|
||||
getEvents: () => request.get<NoticeEventMeta[]>('/notice-rules/events'),
|
||||
|
||||
runScan: () => request.post<NoticeScanResult>('/notice-rules/run-scan'),
|
||||
|
||||
getDetail: (id: number) => request.get<NoticeRule>(`/notice-rules/${id}`),
|
||||
|
||||
create: (data: NoticeRulePayload) => request.post<NoticeRule>('/notice-rules', data),
|
||||
|
||||
update: (id: number, data: Omit<NoticeRulePayload, 'ruleKey'>) =>
|
||||
request.put<NoticeRule>(`/notice-rules/${id}`, data),
|
||||
|
||||
updateEnabled: (id: number, enabled: 0 | 1) =>
|
||||
request.put(`/notice-rules/${id}/enabled`, { enabled }),
|
||||
|
||||
remove: (id: number) => request.delete(`/notice-rules/${id}`),
|
||||
};
|
||||
@@ -373,18 +373,47 @@ function openNoticeDrawer() {
|
||||
async function handleNoticeClick(item: NoticeInfo) {
|
||||
if (item.isRead === 0) {
|
||||
try {
|
||||
await noticeApi.markRead(item.id)
|
||||
item.isRead = 1
|
||||
fetchUnreadCount()
|
||||
await noticeApi.markRead(item.id);
|
||||
item.isRead = 1;
|
||||
fetchUnreadCount();
|
||||
} catch (error) {
|
||||
// 标记失败不阻断后续跳转
|
||||
}
|
||||
}
|
||||
// 电工资质到期提醒 → 跳转电工登记管理
|
||||
if (item.bizType === 'worker_cert') {
|
||||
noticeVisible.value = false
|
||||
router.push('/dispatch/worker')
|
||||
const target = resolveNoticeRoute(item);
|
||||
if (target) {
|
||||
noticeVisible.value = false;
|
||||
router.push(target);
|
||||
}
|
||||
// 无映射关系时仅标记已读,不做跳转
|
||||
}
|
||||
|
||||
// 通知类型 / 业务类型 → 目标路由映射(无匹配返回 null)
|
||||
const NOTICE_TYPE_ROUTE: Record<string, string> = {
|
||||
// 电工资质证照到期提醒
|
||||
cert_expire: '/dispatch/worker',
|
||||
// 电工入驻审核结果通知
|
||||
worker_audit_result: '/dispatch/worker',
|
||||
// 兼容旧数据
|
||||
worker_cert: '/dispatch/worker',
|
||||
};
|
||||
|
||||
// 派单通知按工单业务类型跳转对应派单页
|
||||
const NOTICE_BIZ_ROUTE: Record<string, string> = {
|
||||
repair_dispatch: '/dispatch/repair',
|
||||
maintenance_order: '/dispatch/maintenance',
|
||||
install_order: '/project/install',
|
||||
survey_order: '/project/survey',
|
||||
};
|
||||
|
||||
function resolveNoticeRoute(item: NoticeInfo): string | null {
|
||||
const byType = NOTICE_TYPE_ROUTE[item.noticeType];
|
||||
if (byType) return byType;
|
||||
if (item.noticeType === 'dispatch_assign' && item.bizType) {
|
||||
return NOTICE_BIZ_ROUTE[item.bizType] || null;
|
||||
}
|
||||
// 业务类型兜底
|
||||
return item.bizType ? NOTICE_BIZ_ROUTE[item.bizType] || NOTICE_TYPE_ROUTE[item.bizType] || null : null;
|
||||
}
|
||||
|
||||
async function handleReadAll() {
|
||||
|
||||
@@ -179,7 +179,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "dispatch/cost",
|
||||
name: "DispatchCost",
|
||||
component: () => import("@/views/dispatch/cost/index.vue"),
|
||||
meta: { requiresAuth: true, title: "电工派单成本价", permission: "dispatch:cost" }
|
||||
meta: { requiresAuth: true, title: "勘察费用", permission: "dispatch:cost" }
|
||||
},
|
||||
{
|
||||
path: "dispatch/worker",
|
||||
@@ -248,6 +248,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/system/job/index.vue"),
|
||||
meta: { requiresAuth: true, title: "定时任务", permission: "system:job" }
|
||||
},
|
||||
{
|
||||
path: "system/notice",
|
||||
name: "SystemNotice",
|
||||
component: () => import("@/views/system/notice/index.vue"),
|
||||
meta: { requiresAuth: true, title: "通知管理", permission: "notice:rule" }
|
||||
},
|
||||
{
|
||||
path: "log/operation",
|
||||
name: "OperationLog",
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>运营派单</span>
|
||||
<span>项目管理</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">电工派单成本价</span>
|
||||
<span class="cur">勘察费用</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">电工派单成本价</span>
|
||||
<span class="sub-title">DISPATCH COST PRICE</span>
|
||||
<span class="main-title">勘察费用</span>
|
||||
<span class="sub-title">SURVEY FEE</span>
|
||||
</div>
|
||||
<p class="desc">维护共享电工派单的成本价目,按派单类型区分单价或总价提成,支持按次、按工时两种计价方式。</p>
|
||||
<p class="desc">维护勘察派单相关费用标准(基础出场费、回路测点补贴等),支持按次、按工时两种计价方式。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button v-if="canCreate" type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增成本价
|
||||
新增勘察费用
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">电工派单成本价清单</span>
|
||||
<span class="t">勘察费用清单</span>
|
||||
<span class="count">{{ total }}</span>
|
||||
</div>
|
||||
<a-button type="text" size="small" title="刷新列表" @click="fetchList">
|
||||
@@ -152,7 +152,7 @@
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
v-if="canDelete"
|
||||
title="确定要删除该成本价吗?"
|
||||
title="确定要删除该勘察费用吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
ok-button-props="{ danger: true }"
|
||||
@@ -168,7 +168,7 @@
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无成本价数据" />
|
||||
<a-empty description="暂无勘察费用数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
@@ -203,7 +203,7 @@
|
||||
<div class="section-head">
|
||||
<span class="section-no">01</span>
|
||||
<div class="section-text">
|
||||
<span class="section-name">成本价信息</span>
|
||||
<span class="section-name">勘察费用信息</span>
|
||||
<span class="section-sub">Cost Price Information</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -367,7 +367,7 @@ async function fetchList() {
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取电工派单成本价列表失败');
|
||||
message.error('获取勘察费用列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -399,7 +399,7 @@ function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
editCode.value = '';
|
||||
modalTitle.value = '新增成本价';
|
||||
modalTitle.value = '新增勘察费用';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
@@ -408,7 +408,7 @@ function handleEdit(record: DispatchPriceInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
editCode.value = record.priceCode;
|
||||
modalTitle.value = '编辑成本价';
|
||||
modalTitle.value = '编辑勘察费用';
|
||||
resetForm();
|
||||
|
||||
formData.dispatchType = record.dispatchType || undefined;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div class="receiver-editor">
|
||||
<div class="re-section">
|
||||
<div class="re-section-title">管理端用户</div>
|
||||
<div class="re-field">
|
||||
<label class="re-label">按角色</label>
|
||||
<a-select
|
||||
v-model:value="config.users.roles"
|
||||
mode="multiple"
|
||||
placeholder="选择角色编码(可多选)"
|
||||
allow-clear
|
||||
:options="roleOptions"
|
||||
:loading="roleLoading"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="re-field">
|
||||
<label class="re-label">按用户</label>
|
||||
<a-select
|
||||
v-model:value="config.users.ids"
|
||||
mode="multiple"
|
||||
placeholder="选择具体用户(可多选)"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="userOptions"
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="re-section">
|
||||
<div class="re-section-title">电工</div>
|
||||
<div class="re-field re-field-block">
|
||||
<label class="re-label">接收方式</label>
|
||||
<a-radio-group v-model:value="config.workers.mode" class="re-radio">
|
||||
<a-radio value="none">不发送</a-radio>
|
||||
<a-radio v-if="allowEventMode" value="event">事件关联电工</a-radio>
|
||||
<a-radio value="all">全部在岗电工</a-radio>
|
||||
<a-radio value="ids">指定电工</a-radio>
|
||||
<a-radio value="status">按状态筛选</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="config.workers.mode === 'ids'" class="re-field">
|
||||
<label class="re-label">指定电工</label>
|
||||
<a-select
|
||||
v-model:value="config.workers.ids"
|
||||
mode="multiple"
|
||||
placeholder="选择电工(可多选)"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="workerOptions"
|
||||
:loading="workerLoading"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="config.workers.mode === 'status'" class="re-field">
|
||||
<label class="re-label">审核状态</label>
|
||||
<a-select
|
||||
v-model:value="config.workers.auditStatus"
|
||||
placeholder="全部审核状态"
|
||||
allow-clear
|
||||
:options="auditOptions"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="config.workers.mode === 'status'" class="re-field">
|
||||
<label class="re-label">在岗状态</label>
|
||||
<a-select
|
||||
v-model:value="config.workers.status"
|
||||
placeholder="全部在岗状态"
|
||||
allow-clear
|
||||
:options="statusOptions"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { roleApi } from '@/api/role';
|
||||
import { userApi } from '@/api/user';
|
||||
import { workerApi } from '@/api/worker';
|
||||
import type { ReceiverConfig } from '@/api/noticeRule';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// 直接持有的响应式接收对象配置(嵌套字段就地修改,父级实时感知)
|
||||
config: ReceiverConfig;
|
||||
// 手动群发无「事件关联」上下文,可关闭该选项
|
||||
allowEventMode?: boolean;
|
||||
}>(),
|
||||
{ allowEventMode: true }
|
||||
);
|
||||
|
||||
const roleOptions = ref<{ value: string; label: string }[]>([]);
|
||||
const userOptions = ref<{ value: number; label: string }[]>([]);
|
||||
const workerOptions = ref<{ value: number; label: string }[]>([]);
|
||||
|
||||
const roleLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const workerLoading = ref(false);
|
||||
|
||||
const auditOptions = [
|
||||
{ value: 0, label: '待审核' },
|
||||
{ value: 1, label: '已通过' },
|
||||
{ value: 2, label: '已驳回' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 1, label: '在岗' },
|
||||
{ value: 0, label: '离职' },
|
||||
];
|
||||
|
||||
// 角色/用户/电工下拉为可选增强项,拉取失败(如无对应菜单权限)不阻断配置
|
||||
async function loadRoles() {
|
||||
roleLoading.value = true;
|
||||
try {
|
||||
const res = await roleApi.getAllRoles();
|
||||
if (res.code === 200) {
|
||||
roleOptions.value = (res.data || []).map((r) => ({
|
||||
value: r.roleCode,
|
||||
label: `${r.roleName}(${r.roleCode})`,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// 忽略:无权限时由用户手动维护角色编码
|
||||
} finally {
|
||||
roleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const res = await userApi.getUserList({ page: 1, pageSize: 200 });
|
||||
if (res.code === 200) {
|
||||
userOptions.value = (res.data?.list || []).map((u) => ({
|
||||
value: u.id,
|
||||
label: `${u.realName || u.username}(${u.username})`,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkers() {
|
||||
workerLoading.value = true;
|
||||
try {
|
||||
const res = await workerApi.getOptions();
|
||||
if (res.code === 200) {
|
||||
workerOptions.value = (res.data || []).map((w) => ({
|
||||
value: w.id,
|
||||
label: w.workerTitle ? `${w.workerName}(${w.workerTitle})` : w.workerName,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
} finally {
|
||||
workerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRoles();
|
||||
loadUsers();
|
||||
loadWorkers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receiver-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.re-section {
|
||||
border: 1px solid #eef1f6;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
background: #f8faff;
|
||||
}
|
||||
|
||||
.re-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.re-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.re-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.re-field-block {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.re-label {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.re-radio {
|
||||
line-height: 2;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user