推送管理

This commit is contained in:
王兴凯 2026-07-10 17:11:58 +08:00
parent 948009a314
commit 890584921f
11 changed files with 2202 additions and 1067 deletions

View File

@ -12,9 +12,9 @@ VITE_APP_BASE_API = '/dev-api'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟 # 汤伟
VITE_APP_BASE_URL = 'http://10.84.111.235:8093' # VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
# 李林 # 李林
# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# 测试环境线上10.84.121.122: # 测试环境线上10.84.121.122:
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'

View File

@ -0,0 +1,100 @@
import request from '@/utils/request';
// 获取列表
export function getPushConfigList(data: any) {
return request({
url: '/system/pushConfig/getPushConfigList',
method: 'post',
data
});
}
//新增推送配置
export function addPushConfig(data: any) {
return request({
url: '/system/pushConfig/addPushConfig',
method: 'post',
data
});
}
//修改新增配置
export function deletePushConfig(data: any) {
return request({
url: '/system/pushConfig/deletePushConfig',
method: 'post',
data
});
}
//删除新增配置
export function updatePushConfig(data: any) {
return request({
url: '/system/pushConfig/updatePushConfig',
method: 'post',
data
});
}
//推送目标配置 --用户
export function queryUsersList(data: any) {
return request({
url: '/system/pushConfig/queryUsersList',
method: 'get',
params:data
});
}
//推送目标配置 -- 角色
export function queryRolesList(data: any) {
return request({
url: '/system/pushConfig/queryRolesList',
method: 'get',
params:data
});
}
//查询推送目标
export function getPushTargets(data: any) {
return request({
url: '/system/pushConfig/getPushTargets',
method: 'get',
params:data
});
}
//新增推送目标
export function savePushTargets(data: any) {
return request({
url: '/system/pushConfig/savePushTargets',
method: 'post',
data
});
}
//短信提醒状态修改
export function setPushConfigSmsSwitch(data: any) {
return request({
url: '/system/pushConfig/setPushConfigSmsSwitch',
method: 'post',
data
});
}
//获取推送历史列表
export function postPushHistoryList(data: any) {
return request({
url: '/system/pushConfig/postPushHistoryList',
method: 'post',
data
});
}
//推送情况
export function getPushHistoryDetailList(data: any) {
return request({
url: '/system/pushConfig/getPushHistoryDetailList',
method: 'post',
data
});
}
//s数量
export function getPushHistoryDetailById(data: any) {
return request({
url: '/system/pushConfig/getPushHistoryDetailById',
method: 'get',
params:data
});
}

View File

@ -79,21 +79,21 @@ const data = ref([
name: '正常', name: '正常',
num: '0', num: '0',
warnState: '1', warnState: '1',
icon: 'iconfont icon-shuidianzhan1 zhuangji', icon: 'iconfont icon-map-rgszzDabiao zhuangji',
background: '#78c300', background: '#78c300',
}, },
{ {
name: '告警', name: '告警',
num: '0', num: '0',
warnState: '2', warnState: '2',
icon: 'iconfont icon-shuidianzhanGaojing shebeiZhengchang', icon: 'iconfont icon-map-rgszzDabiao shebeiZhengchang',
background: '#e55555', background: '#e55555',
}, },
{ {
name: '无数据', name: '无数据',
num: '0', num: '0',
warnState: '0', warnState: '0',
icon: 'iconfont icon-shuidianzhanWushujv shebeiLixian', icon: 'iconfont icon-map-rgszzDabiao shebeiLixian',
background: '#8d8d8d', background: '#8d8d8d',
}, },
]); ]);

View File

@ -22,7 +22,7 @@
<a-button type="link" @click="backToLogin" class="back-button"> <a-button type="link" @click="backToLogin" class="back-button">
<span> 返回</span> <span> 返回</span>
</a-button> </a-button>
<h3 class="forgot-password-title">忘记密码</h3> <!-- <h3 class="forgot-password-title">忘记密码</h3> -->
</div> </div>
<a-form <a-form
@ -32,7 +32,11 @@
class="form-container" class="form-container"
> >
<a-form-item label="" name="phone"> <a-form-item label="" name="phone">
<a-input placeholder="请输入手机号" size="large"> <a-input
v-model:value="forgotPasswordForm.phone"
placeholder="请输入手机号"
size="large"
>
<template #prefix> <template #prefix>
<MobileOutlined /> <MobileOutlined />
</template> </template>
@ -42,9 +46,10 @@
<a-form-item label="" name="captcha"> <a-form-item label="" name="captcha">
<div class="captcha-row"> <div class="captcha-row">
<a-input <a-input
v-model="forgotPasswordForm.captcha" v-model:value="forgotPasswordForm.captcha"
placeholder="请输入验证码" placeholder="请输入验证码"
size="large" size="large"
autocomplete="current-captcha"
class="captcha-input" class="captcha-input"
> >
<template #prefix> <template #prefix>
@ -69,7 +74,8 @@
<a-form-item label="" name="newPassword"> <a-form-item label="" name="newPassword">
<a-input-password <a-input-password
v-model="forgotPasswordForm.newPassword" v-model:value="forgotPasswordForm.newPassword"
autocomplete="current-password"
placeholder="请输入新密码" placeholder="请输入新密码"
size="large" size="large"
> >
@ -79,6 +85,19 @@
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<a-form-item label="" name="confirmPassword">
<a-input-password
v-model:value="forgotPasswordForm.confirmPassword"
autocomplete="current-confirmPassword"
placeholder="请再次输入密码"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-button <a-button
type="primary" type="primary"
size="large" size="large"
@ -162,32 +181,51 @@
> >
<span>登录</span> <span>登录</span>
</a-button> </a-button>
<!-- <a-button <div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<a-checkbox v-model:checked="remember" style="margin-top: 10px"
>记住密码</a-checkbox
>
<a-button
type="link" type="link"
size="mini" size="mini"
block
@click="showForgotPasswordPage" @click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }" :style="{ marginTop: '10px', border: 'none' }"
> >
忘记密码 忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
用户注册
</a-button> --> </a-button> -->
</div>
<!-- 忘记密码 --> <!-- 忘记密码 -->
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<!-- 短信登录 Tab (占位) --> <!-- 短信登录 Tab (占位) -->
<!-- <a-tab-pane key="sms" tab="短信登录"> <a-tab-pane key="sms" tab="短信登录">
<a-form <a-form
:model="loginData" :model="loginData"
:rules="loginRules" :rules="state.smsLoginRules"
layout="vertical" layout="vertical"
class="form-container" class="form-container"
@finish="onSmsLogin"
> >
<a-form-item label="" name="username"> <a-form-item label="" name="username">
<a-input <a-input
v-model:value="loginData.username" v-model:value="loginData.username"
autocomplete="current-username"
placeholder="请输入手机号" placeholder="请输入手机号"
size="large" size="large"
> >
@ -199,9 +237,8 @@
<a-form-item label="" name="captcha"> <a-form-item label="" name="captcha">
<div class="captcha-row"> <div class="captcha-row">
<a-input <a-input
v-model="loginData.code" v-model:value="loginData.code"
placeholder="请输入验证码" placeholder="请输入验证码"
autocomplete="current-captcha"
size="large" size="large"
class="captcha-input" class="captcha-input"
> >
@ -235,8 +272,36 @@
> >
<span>登录</span> <span>登录</span>
</a-button> </a-button>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: start;
"
>
<a-button
type="link"
size="mini"
block
@click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }"
style="width: 20%;"
>
忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
block
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
注册
</a-button> -->
</div>
</a-form> </a-form>
</a-tab-pane> --> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
</div> </div>
@ -248,19 +313,23 @@
import { onMounted, reactive, ref, toRefs, watch } from "vue"; import { onMounted, reactive, ref, toRefs, watch } from "vue";
import loginImg from "@/assets/images/logo.png"; import loginImg from "@/assets/images/logo.png";
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue"; import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
import { getCaptcha } from "@/api/auth"; import { getCaptcha, sendSmsCode, smsLoginApi, resetPassword } from "@/api/auth";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import { setToken } from "@/utils/auth";
import Cookies from "js-cookie";
// //
import router from "@/router"; import router from "@/router";
// API // API
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { LoginData } from "@/api/auth/types"; import { LoginData } from "@/api/auth/types";
// //
import { encrypt } from "@/utils/rsaEncrypt"; import { encrypt, decrypt } from "@/utils/rsaEncrypt";
// //
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
// //
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -280,6 +349,7 @@ const forgotPasswordForm = ref({
phone: "", phone: "",
captcha: "", captcha: "",
newPassword: "", newPassword: "",
confirmPassword: "",
}); });
// //
const showForgotPassword = ref(false); const showForgotPassword = ref(false);
@ -299,6 +369,10 @@ const state = reactive({
password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }], password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }], code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
}, },
smsLoginRules: {
username: [{ required: true, trigger: "blur", message: "请输入手机号" }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
},
loginImg: loginImg[0], loginImg: loginImg[0],
loading: false, loading: false,
passwordType: "password", passwordType: "password",
@ -320,15 +394,80 @@ const forgotPasswordRules = ref({
captcha: [{ required: true, message: "验证码不能为空", trigger: "blur" }], captcha: [{ required: true, message: "验证码不能为空", trigger: "blur" }],
newPassword: [ newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" }, { required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 6, message: "密码长度不能少于6位", trigger: "blur" }, { min: 10, message: "密码长度不能少于10位", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) return Promise.resolve();
//
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
return Promise.reject(
new Error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种")
);
}
//
if (/(.)\1{1,}/.test(value)) {
return Promise.reject(new Error("密码不能包含连续重复字符"));
}
// /
for (let i = 0; i < value.length - 1; i++) {
if (/\d/.test(value[i]) && /\d/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的数字"));
}
}
}
// /
for (let i = 0; i < value.length - 1; i++) {
if (/[a-zA-Z]/.test(value[i]) && /[a-zA-Z]/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的字母"));
}
}
}
return Promise.resolve();
},
trigger: "blur",
},
],
confirmPassword: [
// { required: true, message: "", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject(new Error("请再次输入密码"));
}
if (value !== forgotPasswordForm.value.newPassword) {
return Promise.reject(new Error("两次输入的密码不一致"));
}
return Promise.resolve();
},
trigger: "blur",
},
], ],
}); });
const { loginData, loginRules, loading, const {
loginData,
loginRules,
loading,
// passwordType, capslockTooltipDisabled // passwordType, capslockTooltipDisabled
} = toRefs( } = toRefs(state);
state
);
// function checkCapslock(e: any) { // function checkCapslock(e: any) {
// const { key } = e; // const { key } = e;
@ -364,8 +503,19 @@ function onFinish() {
userStore userStore
.login(user) .login(user)
.then(() => { .then(() => {
if (remember.value == true) {
Cookies.set("username", user.username, { expires: 30 });
Cookies.set("password", user.password, { expires: 30 });
Cookies.set("rememberMe", String(remember.value), { expires: 30 });
} else {
// cookie
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
router.push({ path: "/" }); router.push({ path: "/" });
state.loading = false; state.loading = false;
message.success("登录成功");
}) })
.catch(() => { .catch(() => {
getCode(); getCode();
@ -373,6 +523,93 @@ function onFinish() {
}); });
} }
/**
* 短信验证码登录
*/
const onSmsLogin = async () => {
try {
// ========== ==========
const phoneRegex = /^1[3-9]\d{9}$/;
if (!loginData.value.username || !phoneRegex.test(loginData.value.username)) {
message.error("请输入正确的手机号");
return;
}
if (!loginData.value.code || loginData.value.code.trim() === "") {
message.error("请输入验证码");
return;
}
// ========== ==========
state.loading = true;
// ========== API ==========
console.log("开始调用短信登录API, 手机号:", loginData.value.username);
const res: any = await smsLoginApi(
loginData.value.username, //
loginData.value.code //
);
console.log("短信登录API响应:", res);
// ========== ==========
console.log("检查响应结果, res.code:", res.code, "类型:", typeof res.code);
// code "0" 0
if (res.code !== 0 && res.code !== "0") {
console.error("短信登录失败, 错误码:", res.code, "错误信息:", res.msg);
message.error(res.msg || "登录失败");
return;
}
// ========== Token ==========
console.log("开始提取Token, res.data:", res.data);
const token = res.data?.token || res.data; //
if (!token) {
console.error("Token为空, res.data结构:", JSON.stringify(res.data));
message.error("登录失败:未获取到Token");
return;
}
console.log("Token获取成功:", token);
// Cookie
setToken(token);
// Store Token
userStore.Token = token;
console.log("Token已存储到Store");
// ========== ==========
console.log("开始获取用户信息...");
const userInfo = await userStore.getInfo();
console.log("用户信息获取成功:", userInfo);
const roles = userInfo.roles;
console.log("用户角色:", roles);
// ========== ==========
console.log("开始生成路由...");
const permissionStore = usePermissionStoreHook();
const accessRoutes: any = await permissionStore.generateRoutes(roles);
console.log("路由生成成功, 路由数量:", accessRoutes.length);
// ========== ==========
accessRoutes.forEach((route: any) => {
router.addRoute(route);
});
// ========== ==========
router.push({ path: accessRoutes[0].children[0].opturl });
message.success("登录成功");
} catch (error: any) {
// ========== ==========
console.error("短信登录失败", error);
message.error(error.message || "登录失败,请重试");
} finally {
// ========== ==========
state.loading = false;
}
};
watch( watch(
route, route,
() => { () => {
@ -395,6 +632,22 @@ function getOtherQuery(query: any) {
return acc; return acc;
}, {}); }, {});
} }
function getCookie() {
const username = Cookies.get("username");
let password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
rememberMe == "true" ? (remember.value = Boolean(rememberMe)) : false;
// cookie
state.cookiePass = password === undefined ? "" : password;
password = password === undefined ? state.loginData.password : password;
state.loginData = {
username: username === undefined ? state.loginData.username : username,
password: decrypt(password),
code: "",
uuid: "",
};
remember.value = rememberMe === undefined ? false : Boolean(rememberMe);
}
function getCode() { function getCode() {
getCaptcha().then((result: any) => { getCaptcha().then((result: any) => {
codeUrl.value = result.data.img; codeUrl.value = result.data.img;
@ -420,7 +673,10 @@ const startCountdown = () => {
const showForgotPasswordPage = () => { const showForgotPasswordPage = () => {
showForgotPassword.value = true; showForgotPassword.value = true;
}; };
//
const goRegister = () => {
router.push({ path: "/register" });
};
// //
const backToLogin = () => { const backToLogin = () => {
showForgotPassword.value = false; showForgotPassword.value = false;
@ -429,6 +685,7 @@ const backToLogin = () => {
phone: "", phone: "",
captcha: "", captcha: "",
newPassword: "", newPassword: "",
confirmPassword: "",
}; };
}; };
@ -456,10 +713,13 @@ const sendSms = async () => {
smsButtonDisabled.value = true; smsButtonDisabled.value = true;
try { try {
// // type1
// await axios.post('/sms/send', { phone: loginForm.value.username }) const res: any = await sendSmsCode(loginData.value.username, 3);
if (res.code == 1) {
// message.success(res.msg);
return;
}
//
message.success("验证码发送成功"); message.success("验证码发送成功");
// //
@ -496,17 +756,23 @@ const sendForgotPasswordSms = async () => {
smsButtonDisabled.value = true; smsButtonDisabled.value = true;
try { try {
// // type=3
// await axios.post('/sms/forgot-password', { phone: forgotPasswordForm.value.phone }) const res: any = await sendSmsCode(forgotPasswordForm.value.phone, 2);
// if (res.code == 1) {
message.error(res.msg);
smsButtonDisabled.value = false;
return;
}
//
message.success("验证码发送成功"); message.success("验证码发送成功");
// //
startCountdown(); startCountdown();
} catch (error) { } catch (error: any) {
console.error("发送验证码失败", error); // console.error("", error);
message.error("验证码发送失败,请重试"); // message.error(error.message || "");
smsButtonDisabled.value = false; smsButtonDisabled.value = false;
} finally { } finally {
loading.value = false; loading.value = false;
@ -514,23 +780,117 @@ const sendForgotPasswordSms = async () => {
}; };
// //
const handleResetPassword = async () => { const handleResetPassword = async () => {
loading.value = true; //
try { try {
// 2. () //
// await axios.post('/user/reset-password', { const phoneRegex = /^1[3-9]\d{9}$/;
// phone: forgotPasswordForm.value.phone, if (
// captcha: forgotPasswordForm.value.captcha, !forgotPasswordForm.value.phone ||
// newPassword: forgotPasswordForm.value.newPassword !phoneRegex.test(forgotPasswordForm.value.phone)
// }) ) {
message.error("请输入正确的手机号");
return;
}
// 3. if (!forgotPasswordForm.value.captcha) {
message.success("密码重置成功"); message.error("请输入验证码");
return;
}
// 4. if (!forgotPasswordForm.value.newPassword) {
backToLogin(); message.error("请输入新密码");
return;
}
//
if (!forgotPasswordForm.value.confirmPassword) {
message.error("请再次输入密码");
return;
}
//
if (
forgotPasswordForm.value.newPassword !== forgotPasswordForm.value.confirmPassword
) {
message.error("两次输入的密码不一致");
return;
}
//
const password = forgotPasswordForm.value.newPassword;
if (password.length < 10) {
message.error("密码长度不能少于10位");
return;
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
message.error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种");
return;
}
//
if (/(.)\1{1,}/.test(password)) {
message.error("密码不能包含连续重复字符");
return;
}
// /
for (let i = 0; i < password.length - 1; i++) {
if (/\d/.test(password[i]) && /\d/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的数字");
return;
}
}
}
// /
for (let i = 0; i < password.length - 1; i++) {
if (/[a-zA-Z]/.test(password[i]) && /[a-zA-Z]/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的字母");
return;
}
}
}
} catch (error) { } catch (error) {
console.error("密码重置失败", error); console.error("表单验证失败", error);
message.error("密码重置失败,请重试"); return;
}
loading.value = true;
try {
// RSA
const encryptedPassword = encrypt(forgotPasswordForm.value.newPassword);
// 使
const res: any = await resetPassword(
forgotPasswordForm.value.phone,
forgotPasswordForm.value.captcha,
encryptedPassword
);
if (res.code == 0) {
message.success("密码重置成功");
//
backToLogin();
} else {
message.error(res.msg || "密码重置失败");
}
} catch (error: any) {
// console.error("", error);
// message.error(error.message || "");
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -538,6 +898,7 @@ const handleResetPassword = async () => {
onMounted(() => { onMounted(() => {
getCode(); getCode();
getCookie();
}); });
</script> </script>
@ -635,10 +996,12 @@ onMounted(() => {
/* 验证码布局 */ /* 验证码布局 */
.captcha-row { .captcha-row {
display: flex; display: flex;
.ant-row { .ant-row {
display: flex; display: flex;
align-items: start; align-items: start;
} }
/* gap: 12px; /* gap: 12px;
align-items: center; */ align-items: center; */

View File

@ -1,9 +1,9 @@
<template> <template>
<a-modal <a-modal
:title="isEdit ? '编辑沿程配置' : '新增沿程配置'" :title="isEdit ? '修改编辑推送类型' : '新增编辑推送类型'"
v-model:open="modalVisible" v-model:open="modalVisible"
:confirm-loading="localLoading" :confirm-loading="localLoading"
width="1200px" width="35vw"
:destroy-on-close="true" :destroy-on-close="true"
ok-text="保存" ok-text="保存"
@cancel="handleCancel" @cancel="handleCancel"
@ -14,279 +14,105 @@
:model="formData" :model="formData"
:rules="rules" :rules="rules"
name="edit_form" name="edit_form"
:labelCol="{ span: 5 }" :labelCol="{ span: 4 }"
> >
<a-row> <a-row>
<a-col :span="12"> <a-col :span="24">
<a-form-item label="沿程配置名称" name="name"> <a-form-item label="类型名称" name="messageType">
<a-input <a-input
v-model:value="formData.name" v-model:value="formData.messageType"
style="width: 100%" style="width: 100%"
placeholder="请输入沿程配置名称" placeholder="请输入类型名称"
/> </a-form-item
></a-col>
<a-col :span="12">
<a-form-item label="配置编码" name="code">
<a-input
v-model:value="formData.code"
style="width: 100%"
placeholder="请输入配置编码"
/> </a-form-item /> </a-form-item
></a-col> ></a-col>
</a-row> </a-row>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="24">
<a-form-item label="所在河段" name="rvnm"> <a-form-item label="所属类别" name="categoryCode">
<a-select
v-model:value="formData.categoryCode"
placeholder="请选择"
style="width: 100%"
:options="categoryCodeOption"
@change="handleCategoryChange"
></a-select> </a-form-item
></a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item label="关联基地">
<a-select
v-model:value="formData.baseIdList"
mode="multiple"
style="width: 100%"
placeholder="请选择"
:options="baseIdsOption"
@change="handleBaseChange"
></a-select> </a-form-item
></a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item
label="定时任务"
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<a-input <a-input
v-model:value="formData.rvnm" v-model:value="formData.cronExpression"
style="width: 100%" style="width: 70%"
placeholder="请输入所在河段" placeholder="请输入cron表达式"
/> </a-form-item />
></a-col> <a-button style="width: 27%" @click="openCronModal"
<a-col :span="12"> >cron表达式</a-button
<a-form-item label="备注" name="description"> >
<a-textarea </div>
:rows="4" </a-form-item></a-col
v-model:value="formData.description" >
style="width: 100%"
placeholder="请输入备注"
/> </a-form-item
></a-col>
</a-row> </a-row>
</a-form> </a-form>
<div class="config-tree-container"> </a-modal>
<!-- 新增一级节点区域 -->
<div v-if="addingNewNode" class="add-node-row">
<a-select
v-model:value="newNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="handleNodeSelect"
>
<a-select-option
v-for="item in stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click="handleSaveNewNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button @click="handleCancelAdd" style="margin-left: 8px">
<MinusOutlined />
</a-button>
</div>
<a-button v-else class="mb-4" type="primary" @click="handleAdd">
<PlusOutlined /> 新增一级节点
</a-button>
<!-- 树形结构 --> <a-modal
<a-tree title="Cron表达式选择器"
v-if="treeData.length > 0" v-model:open="cronModalVisible"
:tree-data="treeData" width="1200px"
:default-expand-all="false" :destroy-on-close="true"
:selectable="false" footer=""
block-node @cancel="closeCronModal"
style="width: 300px"
> >
<template #title="{ dataRef }"> <CronPicker
<!-- 编辑状态只显示编辑区域 --> v-model="formData.cronExpression"
<div @confirm="handleCronConfirm"
v-if="editingNode && editingNodeKey === dataRef.key" />
class="add-child-node-row w-[300px]"
>
<a-select
v-model:value="editNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="handleEditNodeSelect"
>
<a-select-option
v-for="item in stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click.stop="handleSaveEditNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button
@click.stop="handleCancelEditNode"
style="margin-left: 8px"
>
<MinusOutlined />
</a-button>
</div>
<!-- 非编辑状态显示正常节点内容 -->
<template v-else>
<div class="tree-node-wrapper">
<span class="tree-node-title"> {{ getNodeTitle(dataRef) }} </span>
<div class="tree-node-actions">
<!-- 一级节点支持新增子节点编辑排序删除 -->
<template v-if="!dataRef.parentId">
<a-button
type="link"
size="small"
@click.stop="handleAddChild(dataRef)"
title="添加子节点"
>
<PlusOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleEditNode(dataRef)"
title="编辑"
>
<EditOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveUp(dataRef)"
title="上移"
:disabled="isFirstNode(dataRef)"
>
<ArrowUpOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveDown(dataRef)"
title="下移"
:disabled="isLastNode(dataRef)"
>
<ArrowDownOutlined />
</a-button>
<a-button
type="link"
size="small"
danger
@click.stop="handleDeleteNode(dataRef)"
title="删除"
>
<DeleteOutlined />
</a-button>
</template>
<!-- 二级节点只支持编辑排序删除 -->
<template v-else>
<a-button
type="link"
size="small"
@click.stop="handleEditNode(dataRef)"
title="编辑"
>
<EditOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveUp(dataRef)"
title="上移"
:disabled="isFirstNode(dataRef)"
>
<ArrowUpOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveDown(dataRef)"
title="下移"
:disabled="isLastNode(dataRef)"
>
<ArrowDownOutlined />
</a-button>
<a-button
type="link"
size="small"
danger
@click.stop="handleDeleteNode(dataRef)"
title="删除"
>
<DeleteOutlined />
</a-button>
</template>
</div>
</div>
<!-- 子节点添加区域 -->
<div
v-if="addingChildNode && currentParentKey === dataRef.key"
class="add-child-node-row"
>
<a-select
v-model:value="childNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="handleChildNodeSelect"
>
<a-select-option
v-for="item in stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click.stop="handleSaveChildNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button
@click.stop="handleCancelAddChild"
style="margin-left: 8px"
>
<MinusOutlined />
</a-button>
</div>
</template>
</template>
</a-tree>
<a-empty v-else description="暂无数据" style="margin-top: 20px" />
</div>
</a-modal> </a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'; import { ref, reactive, computed, watch,onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form'; import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import CronPicker from './CronPicker.vue';
PlusOutlined, import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
MinusOutlined, import { getDictItemsByCode } from '@/api/dict';
EditOutlined, import { useDraggable } from '@/utils/drag';
DeleteOutlined,
ArrowUpOutlined,
ArrowDownOutlined
} from '@ant-design/icons-vue';
interface Props { interface Props {
visible: boolean; visible: boolean;
initialValues?: any | null; initialValues?: any | null;
loading?: boolean; loading?: boolean;
} }
const categoryCodeOption:any =ref([])
const localLoading = ref(false); const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
visible: false, visible: false,
@ -309,368 +135,82 @@ const formRef = ref();
const defaultFormData = reactive({ const defaultFormData = reactive({
id: undefined, id: undefined,
name: undefined, messageType: undefined,
code: undefined, categoryCode: undefined,
rvnm: undefined, categoryName: undefined,
description: undefined baseIdList: [],
baseNameList: [],
cronExpression: undefined
}); });
const formData: any = reactive({ ...defaultFormData }); const formData: any = reactive({ ...defaultFormData });
const JidiSelectEventStore = useJidiSelectEventStore(); //jidiData
const baseIdsOption = computed(() => {
const hasAll = (formData.baseIdList || []).includes('all');
return JidiSelectEventStore.jidiData.map(item => ({
value: item.wbsCode,
label: item.wbsName,
disabled: hasAll && item.wbsCode !== 'all'
}));
});
const handleBaseChange = (value: string[]) => {
if ((value || []).includes('all')) {
formData.baseIdList = ['all'];
formData.baseNameList = ['当前全部'];
} else {
formData.baseNameList = (value || []).map(code => {
const item = JidiSelectEventStore.jidiData.find(d => d.wbsCode === code);
return item?.wbsName || '';
});
}
};
const handleCategoryChange = (value: string) => {
const option = categoryCodeOption.value.find(o => o.value === value);
formData.categoryName = option?.label || '';
};
const rules: Record<string, Rule[]> = { const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入沿程配置名称', trigger: 'blur' }], messageType: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入配置编码', trigger: 'blur' }], categoryCode: [{ required: true, message: '请选择所属类别', trigger: 'blur' }]
rvnm: [{ required: true, message: '请输入所在河段', trigger: 'blur' }]
}; };
const isEdit = computed(() => !!props.initialValues); const isEdit = computed(() => !!props.initialValues);
// const cronModalVisible = ref(false);
const treeData = ref<any[]>([]); useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
useDraggable(cronModalVisible, { boundary: true, resetOnOpen: true });
// const openCronModal = () => {
const addingNewNode = ref(false); cronModalVisible.value = true;
const newNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: ''
});
//
const addingChildNode = ref(false);
const currentParentKey = ref<string | null>(null);
const childNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: ''
});
//
const editingNode = ref(false);
const editingNodeKey = ref<string | null>(null);
const editNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: '',
originalData: null as any
});
//
const stationList = ref([
{ stcd: '1', stnm: '站点A', sttpName: '类型1' },
{ stcd: '2', stnm: '站点B', sttpName: '类型2' },
{ stcd: '3', stnm: '站点C', sttpName: '类型1' },
{ stcd: '4', stnm: '站点D', sttpName: '类型3' }
]);
// ID
let nodeIdCounter = 0;
const generateNodeId = () => `node_${Date.now()}_${++nodeIdCounter}`;
//
const getNodeTitle = (dataRef: any) => {
if (!dataRef.parentId) {
// stnm
return dataRef.stnm || '';
} else {
// stnm + sttpName
return `${dataRef.stnm || ''} ${dataRef.sttpName || ''}`;
}
}; };
// const closeCronModal = () => {
const filterOption = (input: string, option: any) => { cronModalVisible.value = false;
return (
option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
);
}; };
// const handleCronConfirm = (value: string) => {
const isFirstNode = (dataRef: any) => { formData.cronExpression = value;
const siblings = getSiblings(dataRef); closeCronModal();
return siblings.length > 0 && siblings[0].key === dataRef.key;
}; };
//
const isLastNode = (dataRef: any) => {
const siblings = getSiblings(dataRef);
return (
siblings.length > 0 && siblings[siblings.length - 1].key === dataRef.key
);
};
//
const getSiblings = (dataRef: any) => {
if (!dataRef.parentId) {
//
return treeData.value;
} else {
// children
const parent = findNodeByKey(treeData.value, dataRef.parentId);
return parent?.children || [];
}
};
// key
const findNodeByKey = (nodes: any[], key: string): any => {
for (const node of nodes) {
if (node.key === key) {
return node;
}
if (node.children && node.children.length > 0) {
const found = findNodeByKey(node.children, key);
if (found) return found;
}
}
return null;
};
//
const handleAdd = () => {
addingNewNode.value = true;
newNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
newNodeData.value.stnm = station.stnm;
newNodeData.value.sttpName = station.sttpName;
}
};
//
const handleSaveNewNode = () => {
if (!newNodeData.value.stcd) {
message.error('请选择站点');
return;
}
const newNode = {
key: generateNodeId(),
stcd: newNodeData.value.stcd,
stnm: newNodeData.value.stnm,
sttpName: newNodeData.value.sttpName,
parentId: null,
children: []
};
treeData.value.push(newNode);
addingNewNode.value = false;
message.success('添加成功');
};
//
const handleCancelAdd = () => {
addingNewNode.value = false;
newNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleAddChild = (parentData: any) => {
addingChildNode.value = true;
currentParentKey.value = parentData.key;
childNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleChildNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
childNodeData.value.stnm = station.stnm;
childNodeData.value.sttpName = station.sttpName;
}
};
//
const handleSaveChildNode = () => {
if (!childNodeData.value.stcd) {
message.error('请选择站点');
return;
}
const parent = findNodeByKey(treeData.value, currentParentKey.value!);
if (!parent) {
message.error('父节点不存在');
return;
}
const newChild = {
key: generateNodeId(),
stcd: childNodeData.value.stcd,
stnm: childNodeData.value.stnm,
sttpName: childNodeData.value.sttpName,
parentId: parent.key,
children: []
};
if (!parent.children) {
parent.children = [];
}
//
parent.children.unshift(newChild);
addingChildNode.value = false;
currentParentKey.value = null;
message.success('添加子节点成功');
};
//
const handleCancelAddChild = () => {
addingChildNode.value = false;
currentParentKey.value = null;
childNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleEditNode = (dataRef: any) => {
editingNode.value = true;
editingNodeKey.value = dataRef.key;
//
editNodeData.value = {
stcd: dataRef.stcd,
stnm: dataRef.stnm,
sttpName: dataRef.sttpName,
originalData: { ...dataRef }
};
};
//
const handleEditNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
editNodeData.value.stnm = station.stnm;
editNodeData.value.sttpName = station.sttpName;
editNodeData.value.stcd = station.stcd;
}
};
//
const handleSaveEditNode = () => {
if (!editNodeData.value.stcd) {
message.error('请选择站点');
return;
}
//
const node = findNodeByKey(treeData.value, editingNodeKey.value!);
if (!node) {
message.error('节点不存在');
return;
}
//
node.stcd = editNodeData.value.stcd;
node.stnm = editNodeData.value.stnm;
node.sttpName = editNodeData.value.sttpName;
editingNode.value = false;
editingNodeKey.value = null;
message.success('编辑成功');
};
//
const handleCancelEditNode = () => {
editingNode.value = false;
editingNodeKey.value = null;
editNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: '',
originalData: null
};
};
//
const handleMoveUp = (dataRef: any) => {
const siblings = getSiblings(dataRef);
const index = siblings.findIndex(item => item.key === dataRef.key);
if (index > 0) {
//
[siblings[index - 1], siblings[index]] = [
siblings[index],
siblings[index - 1]
];
message.success('上移成功');
}
};
//
const handleMoveDown = (dataRef: any) => {
const siblings = getSiblings(dataRef);
const index = siblings.findIndex(item => item.key === dataRef.key);
if (index < siblings.length - 1) {
//
[siblings[index], siblings[index + 1]] = [
siblings[index + 1],
siblings[index]
];
message.success('下移成功');
}
};
//
const handleDeleteNode = (dataRef: any) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除节点"${dataRef.stnm}"吗?`,
okText: '确定',
cancelText: '取消',
onOk: () => {
if (!dataRef.parentId) {
//
const index = treeData.value.findIndex(
item => item.key === dataRef.key
);
if (index > -1) {
treeData.value.splice(index, 1);
}
} else {
//
const parent = findNodeByKey(treeData.value, dataRef.parentId);
if (parent && parent.children) {
const index = parent.children.findIndex(
item => item.key === dataRef.key
);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
message.success('删除成功');
}
});
};
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value.validate(); await formRef.value.validate();
const submitValues = {
...formData, const submitData: any = { ...formData };
captureDate: formData.captureDate
? dayjs(formData.captureDate).format('YYYY-MM-DD') if ((formData.baseIdList || []).includes('all')) {
: undefined, const allCodes = JidiSelectEventStore.jidiData
configNodes: treeData.value // .filter(item => item.wbsCode !== 'all')
}; .map(item => item.wbsCode);
emit('ok', submitValues); const allNames = JidiSelectEventStore.jidiData
.filter(item => item.wbsCode !== 'all')
.map(item => item.wbsName);
submitData.baseIdList = allCodes.join(',');
submitData.baseNameList = allNames.join(',');
} else {
submitData.baseIdList = (formData.baseIdList || []).join(',');
submitData.baseNameList = (formData.baseNameList || []).join(',');
}
emit('ok', submitData);
} catch (error) { } catch (error) {
console.error('Validate Failed:', error); console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确'); message.error('请检查表单填写是否正确');
@ -678,32 +218,46 @@ const handleOk = async () => {
}; };
const initForm = () => { const initForm = () => {
resetForm();
if (props.initialValues) { if (props.initialValues) {
const values = props.initialValues; const values = props.initialValues;
Object.keys(formData).forEach(key => { Object.keys(formData).forEach(key => {
if (values.hasOwnProperty(key)) { if (values.hasOwnProperty(key)) {
if (key === 'captureDate' && values[key]) { if (key === 'captureDate' && values[key]) {
formData[key] = dayjs(values[key]); formData[key] = dayjs(values[key]);
} else if (key === 'baseIdList' || key === 'baseNameList') {
formData[key] = values[key] || [];
} else { } else {
formData[key] = values[key]; formData[key] = values[key];
} }
} }
}); });
// if (values.baseIdList) {
if (values.configNodes && Array.isArray(values.configNodes)) { const baseIds = typeof values.baseIdList === 'string'
treeData.value = values.configNodes.map((node: any) => ({ ? values.baseIdList.split(',')
...node, : (Array.isArray(values.baseIdList) ? values.baseIdList : []);
children: const allBaseCodes = JidiSelectEventStore.jidiData
node.children?.map((child: any) => ({ .filter(item => item.wbsCode !== 'all')
...child .map(item => item.wbsCode);
})) || [] const isAllSelected = allBaseCodes.every(code => baseIds.includes(code));
}));
if (isAllSelected) {
formData.baseIdList = ['all'];
formData.baseNameList = ['当前全部'];
} else { } else {
treeData.value = []; formData.baseIdList = baseIds;
formData.baseNameList = baseIds.map((code: string) => {
const item = JidiSelectEventStore.jidiData.find(d => d.wbsCode === code);
return item?.wbsName || '';
});
}
}
if (values.categoryCode) {
const option = categoryCodeOption.value.find(o => o.value === values.categoryCode);
formData.categoryName = option?.label || '';
} }
} else {
resetForm();
} }
}; };
@ -712,8 +266,6 @@ const resetForm = () => {
formRef.value.resetFields(); formRef.value.resetFields();
} }
Object.assign(formData, defaultFormData); Object.assign(formData, defaultFormData);
treeData.value = [];
addingNewNode.value = false;
}; };
const handleCancel = () => { const handleCancel = () => {
@ -732,6 +284,17 @@ watch(
{ immediate: false } { immediate: false }
); );
const initOption = async () => {
let res = await getDictItemsByCode({dictCode: 'category'});
categoryCodeOption.value = res.data.map(item => ({
value: item.itemCode,
label: item.dictName
}));
};
onMounted(() => {
initOption();
});
defineExpose({ defineExpose({
localLoading localLoading
}); });

View File

@ -4,6 +4,7 @@
ref="basicSearchRef" ref="basicSearchRef"
:searchList="searchList" :searchList="searchList"
:initial-values="initSearchData" :initial-values="initSearchData"
:showSearch="false"
@reset="handleReset" @reset="handleReset"
@finish="onSearchFinish" @finish="onSearchFinish"
@values-change="onValuesChange" @values-change="onValuesChange"
@ -38,22 +39,22 @@ const initSearchData = {
const searchData = ref<any>({ ...initSearchData }); const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [ const searchList: any = computed(() => [
{ // {
type: "Input", // type: "Input",
name: "name", // name: "name",
label: "沿程配置名称", // label: "沿",
fieldProps: { // fieldProps: {
allowClear: true, // allowClear: true,
}, // },
}, // },
{ // {
type: "Input", // type: "Input",
name: "rvnm", // name: "rvnm",
label: "所在河段", // label: "",
fieldProps: { // fieldProps: {
allowClear: true, // allowClear: true,
}, // },
}, // },
]); ]);
const onSearchFinish = (values: any) => { const onSearchFinish = (values: any) => {

View File

@ -0,0 +1,707 @@
<template>
<div class="cron-picker">
<a-tabs v-model:activeKey="activeField" type="card" :centered="true">
<a-tab-pane v-for="field in fields" :key="field.key" :tab="field.label">
<div class="field-content">
<a-radio-group
v-model:value="fieldData[field.key].type"
button-style="outline"
size="small"
>
<a-space direction="vertical" :size="12">
<a-radio v-if="field.key !== 'year'" value="all">
<span class="option-label"
>{{ field.allLabel }} 允许的通配符[,-*]</span
>
</a-radio>
<a-radio v-if="field.key === 'year'" value="all">
<span class="option-label">不指定 允许的通配符[,-*]</span>
</a-radio>
<a-radio
v-if="
field.key === 'day' ||
field.key === 'month' ||
field.key === 'week'
"
value="none"
>
<span class="option-label">不指定</span>
</a-radio>
<a-radio v-if="field.key === 'year'" value="yearly">
<span class="option-label">每年</span>
</a-radio>
<a-radio
v-if="field.key !== 'day' && field.key !== 'year'"
value="range"
>
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].rangeStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].rangeEnd"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span v-if="field.key !== 'week'" class="option-label">{{
field.unit
}}</span>
<span v-else class="option-label">到星期</span>
</a-radio>
<a-radio v-if="field.key === 'year'" value="range">
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].rangeStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].rangeEnd"
:min="field.min"
:max="field.max"
class="range-input"
/>
</a-radio>
<a-radio
v-if="field.key !== 'day' && field.key !== 'year'"
value="interval"
>
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label">{{ field.unit }}开始</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStep"
:min="1"
:max="field.max"
class="range-input"
/>
<span class="option-label">{{ field.unit }}执行一次</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="interval">
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label">日开始</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStep"
:min="1"
:max="field.max"
class="range-input"
/>
<span class="option-label">日执行一次</span>
</a-radio>
<a-radio v-if="field.key === 'week'" value="nthWeek">
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].nthWeek"
:min="1"
:max="5"
class="range-input"
/>
<span class="option-label">周的星期</span>
<a-input-number
v-model:value="fieldData[field.key].nthWeekDay"
:min="1"
:max="7"
class="range-input"
/>
</a-radio>
<a-radio v-if="field.key === 'week'" value="lastWeekDay">
<span class="option-label">本月最后一个星期</span>
<a-input-number
v-model:value="fieldData[field.key].lastWeekDay"
:min="1"
:max="7"
class="range-input"
/>
<span class="option-label">(周w)</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="weekday">
<span class="option-label">每月</span>
<a-input-number
v-model:value="fieldData[field.key].weekdayDay"
:min="1"
:max="31"
class="range-input"
/>
<span class="option-label">号最近的那个工作日</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="lastDay">
<span class="option-label">本月最后一天</span>
</a-radio>
<a-radio
v-if="field.key !== 'year'"
value="specify"
class="zhding"
>
<span class="option-label" style="min-width: 30px;">指定</span>
<div v-if="field.key === 'hour'" class="hour-grid">
<a-checkbox-group
v-model:value="fieldData[field.key].specifyValues"
>
<div>
<span class="grid-label">上午:</span>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getHourValues(0, 11)"
:key="val"
:value="val.toString()"
>
{{ formatHour(val) }}
</a-checkbox>
</a-space>
</div>
<div>
<span class="grid-label">下午:</span>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getHourValues(12, 23)"
:key="val"
:value="val.toString()"
>
{{ formatHour(val) }}
</a-checkbox>
</a-space>
</div>
</a-checkbox-group>
</div>
<div v-else class="specify-grid">
<a-checkbox-group
v-model:value="fieldData[field.key].specifyValues"
>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getFieldValues(field)"
:key="val"
:value="val.toString()"
>
{{
formatValue(
val,
field.key === 'second' ||
field.key === 'minute' ||
field.key === 'hour'
)
}}
</a-checkbox>
</a-space>
</a-checkbox-group>
</div>
</a-radio>
</a-space>
</a-radio-group>
</div>
</a-tab-pane>
</a-tabs>
<div class="cron-preview">
<a-alert type="info" :message="`Cron表达式: ${currentCron}`" showIcon />
</div>
<div class="cron-actions">
<a-button @click="handleReset">重置</a-button>
<a-button type="primary" @click="handleConfirm">确认选择</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
interface Props {
modelValue?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'confirm', value: string): void;
}>();
const fields = [
{ key: 'second', label: '秒', min: 0, max: 59, unit: '秒', allLabel: '每秒' },
{
key: 'minute',
label: '分钟',
min: 0,
max: 59,
unit: '分钟',
allLabel: '每分钟'
},
{
key: 'hour',
label: '小时',
min: 0,
max: 23,
unit: '小时',
allLabel: '每小时'
},
{ key: 'day', label: '日', min: 1, max: 31, unit: '日', allLabel: '每日' },
{ key: 'month', label: '月', min: 1, max: 12, unit: '月', allLabel: '每月' },
{ key: 'week', label: '周', min: 1, max: 7, unit: '周', allLabel: '每周' },
{
key: 'year',
label: '年',
min: 1970,
max: 2099,
unit: '年',
allLabel: '每年'
}
];
const activeField = ref('second');
const fieldData = reactive({
second: {
type: 'all',
rangeStart: 0,
rangeEnd: 59,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
minute: {
type: 'all',
rangeStart: 0,
rangeEnd: 59,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
hour: {
type: 'all',
rangeStart: 0,
rangeEnd: 23,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
day: {
type: 'all',
rangeStart: 1,
rangeEnd: 31,
intervalStart: 1,
intervalStep: 1,
weekdayDay: 1,
specifyValues: []
},
month: {
type: 'all',
rangeStart: 1,
rangeEnd: 12,
intervalStart: 1,
intervalStep: 1,
specifyValues: []
},
week: {
type: 'all',
rangeStart: 1,
rangeEnd: 7,
intervalStart: 1,
intervalStep: 1,
nthWeek: 1,
nthWeekDay: 1,
lastWeekDay: 1,
specifyValues: []
},
year: {
type: 'all',
rangeStart: 1970,
rangeEnd: 2099,
intervalStart: 1970,
intervalStep: 1,
specifyValues: []
}
});
const getFieldValues = (field: any) => {
const values: number[] = [];
for (let i = field.min; i <= field.max; i++) {
values.push(i);
}
return values;
};
const getHourValues = (start: number, end: number) => {
const values: number[] = [];
for (let i = start; i <= end; i++) {
values.push(i);
}
return values;
};
const formatValue = (val: number, pad: boolean) => {
return pad ? val.toString().padStart(2, '0') : val.toString();
};
const formatHour = (val: number) => {
return val.toString().padStart(2, '0');
};
const isDaySpecific = () => {
const day = fieldData.day;
return day.type !== 'all' && day.type !== 'lastDay';
};
const isWeekSpecific = () => {
const week = fieldData.week;
return week.type !== 'all' && week.type !== 'none';
};
const generateFieldValue = (key: string) => {
const data = fieldData[key];
switch (data.type) {
case 'all':
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
if (key === 'year') return '';
return '*';
case 'none':
return '?';
case 'yearly':
return '*';
case 'range':
return `${data.rangeStart}-${data.rangeEnd}`;
case 'interval':
return `${data.intervalStart}/${data.intervalStep}`;
case 'nthWeek':
return `${data.nthWeek}#${data.nthWeekDay}`;
case 'lastWeekDay':
return `L#${data.lastWeekDay}`;
case 'weekday':
return `W${data.weekdayDay}`;
case 'lastDay':
return 'L';
case 'specify':
if (data.specifyValues.length === 0) {
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
return '*';
}
return data.specifyValues
.sort((a: string, b: string) => parseInt(a) - parseInt(b))
.join(',');
default:
return '*';
}
};
const currentCron = computed(() => {
const values = [
generateFieldValue('second'),
generateFieldValue('minute'),
generateFieldValue('hour'),
generateFieldValue('day'),
generateFieldValue('month'),
generateFieldValue('week')
];
const yearValue = generateFieldValue('year');
if (yearValue) {
values.push(yearValue);
}
return values.join(' ');
});
const validateCron = (
expression: string
): { valid: boolean; errorMessage: string } => {
const parts = expression.split(' ');
if (parts.length !== 6 && parts.length !== 7) {
return { valid: false, errorMessage: '表达式格式错误应为6或7个字段' };
}
const [second, minute, hour, day, month, week, year] = parts;
const isNumber = (val: string) => /^\d+$/.test(val);
const isWildcard = (val: string) => val === '*' || val === '?';
const isRange = (val: string) => /^\d+-\d+$/.test(val);
const isInterval = (val: string) => /^\d+\/\d+$/.test(val);
const isList = (val: string) => /^(\d+,)+\d+$/.test(val);
const validateRange = (
val: string,
min: number,
max: number,
label: string
) => {
if (isWildcard(val)) return true;
if (isNumber(val)) {
const num = parseInt(val);
return num >= min && num <= max;
}
if (isRange(val)) {
const [start, end] = val.split('-').map(Number);
return start >= min && end <= max && start <= end;
}
if (isInterval(val)) {
const [start, step] = val.split('/').map(Number);
return start >= min && start <= max && step >= 1;
}
if (isList(val)) {
return val.split(',').every(v => {
const num = parseInt(v);
return num >= min && num <= max;
});
}
return false;
};
if (!validateRange(second, 0, 59, '秒')) {
return { valid: false, errorMessage: '秒字段值无效范围应为0-59' };
}
if (!validateRange(minute, 0, 59, '分钟')) {
return { valid: false, errorMessage: '分钟字段值无效范围应为0-59' };
}
if (!validateRange(hour, 0, 23, '小时')) {
return { valid: false, errorMessage: '小时字段值无效范围应为0-23' };
}
if (
!isWildcard(day) &&
!isRange(day) &&
!isInterval(day) &&
!isList(day) &&
!day.startsWith('W') &&
day !== 'L'
) {
const num = parseInt(day);
if (isNaN(num) || num < 1 || num > 31) {
return { valid: false, errorMessage: '日字段值无效范围应为1-31' };
}
}
if (!validateRange(month, 1, 12, '月')) {
return { valid: false, errorMessage: '月字段值无效范围应为1-12' };
}
if (
!isWildcard(week) &&
!isRange(week) &&
!isList(week) &&
!week.includes('#') &&
!week.startsWith('L')
) {
const num = parseInt(week);
if (isNaN(num) || num < 1 || num > 7) {
return { valid: false, errorMessage: '周字段值无效范围应为1-7' };
}
}
if (year && !validateRange(year, 1970, 2099, '年')) {
return { valid: false, errorMessage: '年字段值无效范围应为1970-2099' };
}
if (day !== '?' && week !== '?') {
return {
valid: false,
errorMessage: '日和周不能同时指定,请将其中一个设为"不指定"'
};
}
return { valid: true, errorMessage: '' };
};
const handleConfirm = () => {
const { valid, errorMessage } = validateCron(currentCron.value);
if (!valid) {
message.error(errorMessage);
return;
}
emit('confirm', currentCron.value);
};
const handleReset = () => {
Object.keys(fieldData).forEach(key => {
const field = fields.find(f => f.key === key);
if (field) {
fieldData[key as keyof typeof fieldData] = {
type: key === 'year' ? 'all' : 'all',
rangeStart: field.min,
rangeEnd: field.max,
intervalStart: field.min,
intervalStep: 1,
weekdayDay: 1,
nthWeek: 1,
nthWeekDay: 1,
lastWeekDay: 1,
specifyValues: []
};
}
});
activeField.value = 'second';
};
const parseCron = (expression: string) => {
const parts = expression.split(' ');
const keys = ['second', 'minute', 'hour', 'day', 'month', 'week'];
keys.forEach((key, index) => {
const value = parts[index] || '*';
const data = fieldData[key as keyof typeof fieldData];
if (value === '*') {
data.type = 'all';
} else if (value === '?') {
data.type = 'none';
} else if (value.includes('-')) {
data.type = 'range';
const [start, end] = value.split('-').map(Number);
data.rangeStart = start;
data.rangeEnd = end;
} else if (value.includes('/')) {
data.type = 'interval';
const [start, step] = value.split('/').map(Number);
data.intervalStart = start;
data.intervalStep = step;
} else if (value.includes('#')) {
data.type = 'nthWeek';
const [nth, day] = value.split('#').map(Number);
data.nthWeek = nth;
data.nthWeekDay = day;
} else if (value.startsWith('L#')) {
data.type = 'lastWeekDay';
data.lastWeekDay = parseInt(value.substring(2));
} else if (value.startsWith('W')) {
data.type = 'weekday';
data.weekdayDay = parseInt(value.substring(1));
} else if (value === 'L') {
data.type = 'lastDay';
} else if (value.includes(',')) {
data.type = 'specify';
data.specifyValues = value.split(',');
} else {
data.type = 'specify';
data.specifyValues = [value];
}
});
const yearData = fieldData.year;
if (parts.length === 7) {
const yearValue = parts[6];
if (yearValue === '*') {
yearData.type = 'yearly';
} else if (yearValue.includes('-')) {
yearData.type = 'range';
const [start, end] = yearValue.split('-').map(Number);
yearData.rangeStart = start;
yearData.rangeEnd = end;
} else {
yearData.type = 'specify';
yearData.specifyValues = yearValue.includes(',')
? yearValue.split(',')
: [yearValue];
}
} else {
yearData.type = 'all';
}
};
watch(
() => props.modelValue,
val => {
if (val && val.trim()) {
parseCron(val.trim());
} else if (val === undefined || val === null) {
handleReset();
}
},
{ immediate: true }
);
</script>
<style scoped lang="scss">
.cron-picker {
padding: 8px;
}
.field-content {
padding: 16px 0;
}
.option-label {
margin-right: 4px;
}
.range-input {
width: 60px;
margin: 0 4px;
}
.checkbox-grid {
max-width: 100%;
}
.hour-grid {
display: flex;
flex-direction: column;
gap: 8px;
// margin-top: 8px;
.grid-label {
font-size: 14px;
color: #000;
display: inline-block;
margin: 0px 10px 0px 5px;
}
}
.specify-grid {
// margin-top: 8px;
}
.cron-preview {
margin-top: 16px;
}
.cron-actions {
margin-top: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
:deep(.ant-radio-wrapper span.ant-radio + *) {
display: flex;
align-items: center;
}
.zhding {
:deep(span.ant-radio + *) {
display: flex !important;
align-items: flex-start !important;
// margin-top: px;
}
:deep(.ant-radio){
align-self:auto !important;
// margin-top: 0px !important;
}
}
</style>

View File

@ -0,0 +1,198 @@
<template>
<a-modal
:open="visible"
title="推送目标配置"
:width="780"
:footer="null"
@cancel="handleCancel"
>
<div class="push-target-modal">
<a-radio-group
v-model:value="targetType"
:options="targetOptions"
class="target-radio"
/>
<div v-show="targetType !== '1'" class="transfer-wrapper">
<a-transfer
v-model:target-keys="targetKeys"
v-model:selected-keys="selectedKeys"
:data-source="transferDataSource"
:titles="transferTitles"
:show-search="true"
:filter-option="filterOption"
:render="item => item.title"
:list-style="{ width: '340px', height: '360px' }"
@change="handleChange"
/>
</div>
<div class="modal-footer">
<a-button type="primary" @click="handleSave" :loading="loading">保存</a-button>
<a-button @click="handleCancel">放弃</a-button>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue';
import {queryUsersList,queryRolesList,getPushTargets} from '@/api/system/map/pushManagement';
import { useDraggable } from '@/utils/drag';
const props = defineProps<{
visible: boolean;
initialValues: any;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'cancel'): void;
(
e: 'ok',
values: {
targetType: string;
userRoleIds: string[] | null;
configId?: number;
}
): void;
}>();
const targetOptions = [
{ label: '所有人', value: '1' },
{ label: '按人', value: '2' },
{ label: '按角色类型', value: '3' }
];
const targetType = ref('1');
const selectedKeys = ref<string[]>([]);
const targetKeys = ref<string[]>([]);
const userList = ref<{ key: string; title: string }[]>([]);
const roleList = ref<{ key: string; title: string }[]>([]);
const loading = ref(false);
const isInitializing = ref(false);
const transferReady = ref(false);
const cachedPushTargets = ref<any>(null);
const transferDataSource = computed(() => {
return targetType.value === '2' ? userList.value : roleList.value;
});
const transferTitles = computed(() => {
if (targetType.value === '2') {
return ['未关联用户', '已关联用户'];
}
return ['未关联角色', '已关联角色'];
});
const filterOption = (inputValue: string, item: any) => {
return item.title.toLowerCase().includes(inputValue.toLowerCase());
};
const handleEcho = async (data: any) => {
if (!data) return;
const newTargetType = String(data.targetType ?? '1');
const newUserRoleIds = Array.isArray(data.userRoleIds) ? data.userRoleIds : [];
targetType.value = newTargetType;
if (newTargetType !== '1' && newUserRoleIds.length > 0) {
await nextTick();
const dataSource = newTargetType === '2' ? userList.value : roleList.value;
const validKeys = newUserRoleIds.filter(id =>
dataSource.some(item => item.key === id)
);
targetKeys.value = [...validKeys];
} else {
targetKeys.value = [];
}
};
watch(
() => props.visible,
async val => {
if (val) {
loading.value = true;
targetType.value = '1';
targetKeys.value = [];
selectedKeys.value = [];
await initOption();
if (props.initialValues?.id) {
try {
const resh = await getPushTargets({ configId: props.initialValues.id });
if (resh?.data) {
await handleEcho(resh.data);
}
} catch (error) {
console.error('获取推送目标失败:', error);
}
}
loading.value = false;
}
}
);
const handleChange = (newTargetKeys: string[]) => {
targetKeys.value = newTargetKeys;
};
const handleSave = () => {
let userRoleIds: string[] | null = null;
if (targetType.value === '1') {
userRoleIds = userList.value.map(item => item.key);
} else {
userRoleIds = targetKeys.value as string[];
}
emit('ok', {
targetType: targetType.value,
userRoleIds,
configId: props.initialValues?.id
});
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
};
const initOption = async () => {
let res = await queryUsersList({});
let resjv = await queryRolesList({});
userList.value = (res.data || []).map((item: any) => ({
key: item.id,
title: `${item.nickname || item.username || ''}${item.phone ? '(' + item.phone + ')' : ''}`
}));
roleList.value = (resjv.data || []).map((item: any) => ({
key: item.id,
title: item.rolename || ''
}));
};
onMounted(() => {});
</script>
<style scoped lang="scss">
.push-target-modal {
display: flex;
flex-direction: column;
gap: 20px;
}
.target-radio {
margin-bottom: 4px;
}
.transfer-wrapper {
display: flex;
justify-content: center;
}
.modal-footer {
display: flex;
gap: 12px;
justify-content: flex-start;
}
</style>

View File

@ -7,15 +7,30 @@
:handle-add="handleAdd" :handle-add="handleAdd"
@reset="handleReset" @reset="handleReset"
@search-finish="onSearchFinish" @search-finish="onSearchFinish"
/> /> -->
<div><a-button type="primary" @click="handleAdd">新增</a-button></div>
<BasicTable <BasicTable
ref="basicTable" ref="basicTable"
:columns="columns" :columns="columns"
:list-url="getAllConfigTree" :list-url="getPushConfigList"
:search-params="searchParams" :search-params="searchParams"
> >
<template #isSms="{ column, record }">
<a-switch
v-model:checked="record.isSms"
:checkedValue="'1'"
@change="switchChange(record.isSms, record)"
/>
</template>
<template #action="{ column, record }"> <template #action="{ column, record }">
<div class="flex gap-[6px]"> <div class="flex gap-[6px]">
<a-button
class="!p-0"
type="link"
size="small"
@click="handlePushTarget(record)"
>推送目标</a-button
>
<a-button <a-button
class="!p-0" class="!p-0"
type="link" type="link"
@ -40,79 +55,61 @@
:initial-values="currentRecord" :initial-values="currentRecord"
@cancel="editModalCancel" @cancel="editModalCancel"
@ok="handleEditSubmit" @ok="handleEditSubmit"
/> --> />
<PushTargetModal
ref="pushTargetModal"
v-model:visible="pushTargetModalVisible"
:initial-values="pushTargetInitialValues"
@cancel="pushTargetModalCancel"
@ok="handlePushTargetSubmit"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'; import { ref, nextTick, onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue'; import BasicTable from '@/components/BasicTable/index.vue';
import ConfigManagementSearch from './ConfigManagementSearch.vue';
import ConfigManagementForm from './ConfigManagementForm.vue'; import ConfigManagementForm from './ConfigManagementForm.vue';
import PushTargetModal from './PushTargetModal.vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { import {
getAllConfigTree, getPushConfigList,
deleteBaseWbsb, addPushConfig,
saveBaseWbsb updatePushConfig,
} from '@/api/system/map/ConfigManagement'; deletePushConfig,
savePushTargets,
setPushConfigSmsSwitch
} from '@/api/system/map/pushManagement';
// //
const basicTable = ref<any>(null); const basicTable = ref<any>(null);
// //
const columns = [ const columns = [
{ {
title: '序号', title: '消息类型',
dataIndex: 'index', dataIndex: 'messageType',
key: 'index', key: 'messageType'
width: 60,
align: 'center',
customRender: ({ text, record, index }) => index + 1
}, },
{ {
title: '沿程配置名称', title: '当日推送次数',
dataIndex: 'name', key: 'dailyPushCount',
key: 'name', dataIndex: 'dailyPushCount'
width: 150
}, },
{ {
title: '配置编码', title: '累计推送次数',
key: 'code', dataIndex: 'pushCount',
dataIndex: 'code', key: 'pushCount'
width: 120
}, },
{ {
title: '所在河段', title: '短信提醒',
dataIndex: 'rvnm', key: 'isSms',
key: 'rvnm', dataIndex: 'isSms',
ellipsis: true slots: { customRender: 'isSms' }
},
{
title: '创建人',
key: 'recordUser',
dataIndex: 'recordUser'
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150
},
{
title: '更新时间',
key: 'modifyTime',
dataIndex: 'modifyTime',
width: 150
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 360
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 120, width: 160,
fixed: 'right', fixed: 'right',
slots: { customRender: 'action' } slots: { customRender: 'action' }
} }
@ -124,6 +121,11 @@ const searchParams = ref({});
const currentRecord = ref<any | null>(null); const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false); const editModalVisible = ref(false);
//
const pushTargetModal = ref<any>(null);
const pushTargetModalVisible = ref(false);
const pushTargetInitialValues = ref<any | null>(null);
// //
const handleAdd = () => { const handleAdd = () => {
currentRecord.value = null; currentRecord.value = null;
@ -136,6 +138,33 @@ const handleEdit = (record: any) => {
editModalVisible.value = true; editModalVisible.value = true;
}; };
//
const handlePushTarget = (record: any) => {
pushTargetInitialValues.value = { ...record };
pushTargetModalVisible.value = true;
};
//
const pushTargetModalCancel = () => {
pushTargetInitialValues.value = null;
pushTargetModalVisible.value = false;
};
//
const handlePushTargetSubmit = async (values: any) => {
try {
let res: any = await savePushTargets(values);
if (res.code == 0) {
message.success('保存成功');
}
pushTargetModalVisible.value = false;
basicTable.value.refresh();
} catch (error) {
// message.error('');
return;
}
};
// //
const handleDelete = (record: any) => { const handleDelete = (record: any) => {
Modal.confirm({ Modal.confirm({
@ -144,11 +173,13 @@ const handleDelete = (record: any) => {
zIndex: 2002, zIndex: 2002,
onOk: async () => { onOk: async () => {
try { try {
let res = await deleteBaseWbsb([record.id]); let res = await deletePushConfig([record.id]);
if ((res.code = 0)) {
message.success('删除成功'); message.success('删除成功');
}
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error('删除失败'); // message.error('');
return; return;
} }
} }
@ -156,39 +187,13 @@ const handleDelete = (record: any) => {
}; };
// //
const onSearchFinish = (values: any) => { const onSearchFinish = () => {
const params = { const params = {
logic: 'and', logic: 'and',
filters: [ filters: [].filter(Boolean)
values.parentId
? {
field: 'parentId',
operator: 'eq',
dataType: 'string',
value: values.parentId
}
: null,
values.name
? {
field: 'name',
operator: 'contains',
dataType: 'string',
value: values.name
}
: null
].filter(Boolean)
}; };
basicTable.value.getList(params); basicTable.value.getList(params);
}; };
//
const handleReset = () => {
basicTable.value.getList({
logic: 'and',
filters: []
});
};
// //
const editModalCancel = () => { const editModalCancel = () => {
currentRecord.value = null; currentRecord.value = null;
@ -198,25 +203,55 @@ const editModalCancel = () => {
// //
const handleEditSubmit = async (values: any) => { const handleEditSubmit = async (values: any) => {
try { try {
let res = await saveBaseWbsb({ //updatePushConfig
...currentRecord.value, if (values.id) {
...values let res: any = await updatePushConfig({ ...values });
}); if ((res.code = 0)) {
message.success(`修改成功`);
}
} else {
let res: any = await addPushConfig({ ...values });
if ((res.code = 0)) {
message.success(`保存成功`); message.success(`保存成功`);
}
}
editModalVisible.value = false; editModalVisible.value = false;
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error(`保存失败`); // message.error(``);
editModalVisible.value = false; editModalVisible.value = false;
return; return;
} }
}; };
//
const switchChange = async (checked: any, record: any) => {
Modal.confirm({
title: '确认修改',
content: '确定要修改短信提醒的开关状态吗?',
zIndex: 2002,
onOk: async () => {
try {
let res: any = await setPushConfigSmsSwitch({
id: record.id,
isSms: checked ? '1' : '0'
});
if (res.code == 0) {
message.success(`修改成功`);
basicTable.value.refresh();
}
} catch (error) {
return;
}
}
});
};
const initOption = () => {}; const initOption = async () => {};
onMounted(() => { onMounted(() => {
initOption(); initOption();
onSearchFinish();
}); });
</script> </script>

View File

@ -0,0 +1,202 @@
<template>
<a-modal
v-model:open="visible"
title="推送情况"
:footer="null"
width="1000px"
>
<a-tabs
v-if="resultStatusOption.length > 0"
v-model:activeKey="activeTab"
class="mb-4"
>
<a-tab-pane
v-for="item in resultStatusOption"
:key="item.value"
:tab="item.label"
></a-tab-pane>
</a-tabs>
<a-form layout="inline" class="mb-4">
<a-form-item>
<a-input
v-model:value="searchTarget"
placeholder="请输入推送目标"
style="width: 200px"
allow-clear
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
</a-form-item>
</a-form>
<BasicTable
ref="basicTable"
:columns="columns"
:list-url="getPushHistoryDetailList"
:search-params="searchParams"
:scroll-y="500"
>
<template #status="{ record }">
<span
:class="
record.status === '1'
? 'text-success'
: record.status === '2'
? 'text-error'
: ''
"
>
{{
record.status === '1'
? '推送成功'
: record.status === '2'
? '推送失败'
: '待推送'
}}
</span>
</template>
</BasicTable>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import {
getPushHistoryDetailList,
getPushHistoryDetailById
} from '@/api/system/map/pushManagement';
import { getDictItemsByCode } from '@/api/dict';
import BasicTable from '@/components/BasicTable/index.vue';
import { useDraggable } from '@/utils/drag';
const props = defineProps<{
visible: boolean;
record: any;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const visible = computed({
get: () => props.visible,
set: value => emit('update:visible', value)
});
useDraggable(visible, { boundary: true, resetOnOpen: true });
const searchTarget = ref('');
const activeTab = ref('all');
//
const basicTable = ref<any>(null);
const searchParams = ref({
sort: [
{
field: 'createdAt',
dir: 'desc'
}
]
});
const columns = [
{ title: '推送目标', dataIndex: 'targetName', key: 'targetName' },
{ title: '推送方式', dataIndex: 'sendChannel', key: 'sendChannel' },
{ title: '推送时间', dataIndex: 'sendTime', key: 'sendTime' },
{ title: '推送结果', key: 'status', slots: { customRender: 'status' } }
];
const resultStatusOption = ref([]);
const handleSearch = () => {
if (!basicTable.value || !props.record || !props.record.id) {
return;
}
const filters = [];
filters.push({
field: 'historyId',
operator: 'eq',
dataType: 'string',
value: props.record.id
});
if (activeTab.value !== 'all') {
filters.push({
field: 'status',
operator: 'eq',
dataType: 'string',
value: activeTab.value
});
}
if (searchTarget.value) {
filters.push({
field: 'targetName',
operator: 'contains',
dataType: 'string',
value: searchTarget.value
});
}
const params = {
logic: 'and',
filters
};
basicTable.value.getList(params);
};
const getDict = async () => {
try {
let res = await getDictItemsByCode({ dictCode: 'result_status' });
if (res.data.length > 0) {
resultStatusOption.value = res.data.map((item: any) => ({
value: item.itemCode,
label: item.dictName
}));
resultStatusOption.value.unshift({ value: 'all', label: '全部' });
}
} catch (error) {
console.error('获取字典失败:', error);
}
};
onMounted(async () => {
await getDict();
});
watch(activeTab, () => {
handleSearch();
});
watch(
() => props.record,
async newVal => {
if (newVal && props.visible) {
await nextTick();
await nextTick();
if (basicTable.value) {
handleSearch();
}
let res1 = await getPushHistoryDetailById({ historyId: props.record.id });
const { failCount = 0, pendingCount = 0, successCount = 0 } = res1.data || {};
const total = failCount + pendingCount + successCount;
resultStatusOption.value = resultStatusOption.value.map(item => {
let count = 0;
if (item.value === 'all') count = total;
else if (item.value === '1') count = successCount;
else if (item.value === '2') count = failCount;
else if (item.value === '0') count = pendingCount;
const originalLabel = item.originalLabel || item.label;
return {
...item,
originalLabel,
label: `${originalLabel}(${count})`
};
});
}
},
{ deep: true }
);
defineExpose({
handleSearch
});
</script>
<style scoped lang="scss">
.text-success {
color: #52c41a;
}
.text-error {
color: #ff4d4f;
}
</style>

View File

@ -1,208 +1,161 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LayerManagement\index.vue --> <!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LayerManagement\index.vue -->
<template> <template>
<div class="content"> <div class="content">
推送历史 <!-- 推送历史 -->
<!-- <LayerManagementSearch <a-form layout="inline" class="search-form">
ref="layerManagementSearch" <a-form-item label="">
:handle-add="handleAdd" <a-input
@reset="handleReset" v-model:value="searchForm.title"
@search-finish="onSearchFinish" placeholder="请输入标题关键字"
allow-clear
style="width: 180px"
@pressEnter="onSearchFinish()"
/> />
</a-form-item>
<a-form-item label="起止时间">
<a-range-picker
v-model:value="searchForm.planTime"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="onSearchFinish()"
/>
</a-form-item>
<a-form-item label="">
<a-select
v-model:value="searchForm.messageTypeCode"
placeholder="请选择消息类型"
style="width: 180px"
allow-clear
show-search
:filter-option="filterOption"
:options="messageTypeCodeOption"
@change="onSearchFinish()"
/>
</a-form-item>
<a-form-item label="">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 180px"
allow-clear
show-search
:filter-option="filterOption"
:options="statusOption"
@change="onSearchFinish()"
/>
</a-form-item>
</a-form>
<BasicTable <BasicTable
v-if="statusOption.length > 0"
ref="basicTable" ref="basicTable"
:columns="columns" :columns="columns"
:list-url="getAllMapLayerTree" :list-url="postPushHistoryList"
:search-params="searchParams" :search-params="searchParams"
> >
<template #number="{ record }">
{{ record.pushedCount ?? 0 }}/{{ record.successCount ?? 0 }}/{{
record.pendingCount ?? 0
}}/{{ record.failCount ?? 0 }}
</template>
<template #status="{ record }">
{{ getname(record.status, statusOption) ?? '-' }}
</template>
<template #action="{ record }"> <template #action="{ record }">
<div class="flex gap-[6px]"> <div class="flex gap-[6px]">
<a-button <a-button
class="!p-0" class="!p-0"
type="link" type="link"
size="small" size="small"
@click="handleEdit(record)" @click="showPushContent(record)"
>编辑</a-button >推送内容</a-button
> >
<a-button <a-button class="!p-0" type="link" size="small" @click="showPushStatus(record)"
v-if="record.children && record.children.length == 0" >推送情况</a-button
class="!p-0"
type="link"
size="small"
@click="handleEditInterface(record)"
>编辑接口</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
@click="handleAdd(record)"
>新增子图层</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
> >
</div> </div>
</template> </template>
<template #enable="{ record }">
<a-switch
:checked="record.enable === 1"
@change="(checked: boolean) => handleEnableChange(record, checked)"
/>
</template>
<template #checkable="{ record }">
<a-switch
:checked="record.checkable === 1"
@change="(checked: boolean) => handleCheckableChange(record, checked)"
/>
</template>
</BasicTable> </BasicTable>
<LayerManagementForm
ref="layerManagementForm" <a-modal
v-model:visible="editModalVisible" v-model:open="pushContentVisible"
:initial-values="currentRecord" title="推送内容"
:is-interface="isInterface" :footer="null"
:loading="editLoading" width="600px"
@cancel="editModalCancel" >
@ok="handleEditSubmit" <div class="push-content-detail">
/> --> <div class="detail-item">
<span class="label">类型</span>
<span class="value">{{ pushContentData.type ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">标题</span>
<span class="value">{{ pushContentData.title ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">内容</span>
<span class="value content">{{
pushContentData.content ?? '-'
}}</span>
</div>
<div class="detail-item">
<span class="label">推送时间</span>
<span class="value">{{ pushContentData.planTime ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">来源</span>
<span class="value">{{
getname(pushContentData.source, statusOption) ?? '-'
}}</span>
</div>
<div class="detail-item">
<span class="label">关联</span>
<span class="value">{{ pushContentData.relation ?? '-' }}</span>
</div>
</div>
</a-modal>
<PushStatusModal ref="pushStatusModalRef" v-model:visible="pushStatusVisible" :record="recordArr" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue'; import BasicTable from '@/components/BasicTable/index.vue';
import LayerManagementSearch from './LayerManagementSearch.vue'; import { postPushHistoryList } from '@/api/system/map/pushManagement';
import LayerManagementForm from './LayerManagementForm.vue'; import { getDictItemsByCode } from '@/api/dict';
import { message, Modal } from 'ant-design-vue'; import PushStatusModal from './PushStatusModal/index.vue';
import { import { useDraggable } from '@/utils/drag';
getAllMapLayerTree,
saveMapLayer,
deleteMapLayer
} from '@/api/system/map/LayerManagement';
//
const columns = [ const columns = [
{ {
title: '', title: '生成时间',
dataIndex: '', dataIndex: 'planTime',
key: '', key: 'planTime'
width: 50,
customRender: () => {
return '';
}
}, },
{ {
title: '序号', title: '消息类型',
key: 'index', dataIndex: 'messageType',
width: 60, key: 'messageType'
align: 'center',
customRender: ({ record, index }: any) => {
// children
// parentId
if (!record.parentId) {
// dataSource
let rootIndex = 0;
for (let i = 0; i < basicTable.value.tableData.length; i++) {
const item = basicTable.value.tableData[i];
// parentId
if (!item.parentId) {
rootIndex++;
//
if (
item.id === record.id ||
(item.key === record.key && item.id === record.id)
) {
return rootIndex;
}
}
}
return rootIndex;
} else {
// children
const findParentAndIndex = (data: any[], parentId: string): number => {
for (const item of data) {
if (item.key === parentId || item.id === parentId) {
// children
if (item.children && item.children.length > 0) {
const childIndex = item.children.findIndex(
(child: any) =>
child.key === record.key || child.id === record.id
);
return childIndex + 1;
}
}
//
if (item.children && item.children.length > 0) {
const result = findParentAndIndex(item.children, parentId);
if (result > 0) {
return result;
}
}
}
return 0;
};
return findParentAndIndex(basicTable.value.tableData, record.parentId);
}
}
}, },
{ {
title: '图层名称', title: '标题',
dataIndex: 'title', dataIndex: 'title',
key: 'title', key: 'title'
width: 150 },
{
title: '已推送/成功/待推送/失败',
dataIndex: 'number',
key: 'number',
slots: { customRender: 'number' }
}, },
{ {
title: '图层编码', title: '状态',
dataIndex: 'code', key: 'status',
key: 'code', dataIndex: 'status',
width: 120 slots: { customRender: 'status' }
},
{
title: '标签显示级别',
dataIndex: 'treeLevel',
key: 'treeLevel',
width: 100
},
{
title: '是否启用',
dataIndex: 'enable',
key: 'enable',
width: 80,
slots: { customRender: 'enable' }
},
{
title: '是否可选',
dataIndex: 'checkable',
key: 'checkable',
width: 80,
slots: { customRender: 'checkable' }
},
{
title: '备注',
dataIndex: 'description',
key: 'description'
},
{
title: '创建人',
key: 'displayRecordUser',
dataIndex: 'displayRecordUser'
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150
},
{
title: '更新时间',
key: 'modifyTime',
dataIndex: 'modifyTime',
width: 150
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
@ -211,139 +164,133 @@ const columns = [
slots: { customRender: 'action' } slots: { customRender: 'action' }
} }
]; ];
//
// //
const basicTable = ref<any>(null); const basicTable = ref<any>(null);
// const pushStatusModalRef = ref<any>(null);
const searchParams = ref({}); const searchParams = ref({
// sort: [
const isInterface = ref(false); {
const editModalVisible = ref(false); field: 'createdAt',
const editLoading = ref(false); dir: 'desc'
const parentId = ref<string | null>(null);
const currentRecord = ref<any | null>(null);
//
const handleAdd = (record: any) => {
currentRecord.value = null;
parentId.value = record.id;
editModalVisible.value = true;
};
// '-'
const filterRecord = (record: any) => {
const filtered: any = {};
const keepKeys = ['code']; //
Object.keys(record).forEach(key => {
if (keepKeys.includes(key) || record[key] !== '-') {
filtered[key] = record[key];
} }
]
});
//
const searchForm = reactive({
status: undefined, //
messageTypeCode: undefined, //
title: undefined, //
planTime: undefined //
});
const messageTypeCodeOption = ref([]);
const statusOption = ref([]);
const pushContentVisible = ref(false);
const pushContentData = reactive<any>({
type: '',
title: '',
content: '',
planTime: '',
source: '',
relation: ''
});
const pushStatusVisible = ref(false);
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const onSearchFinish = () => {
const filters = [];
if (searchForm.status !== undefined && searchForm.status !== null) {
filters.push({
field: 'status',
operator: 'eq',
dataType: 'string',
value: searchForm.status
}); });
return filtered;
};
//
const handleEdit = (record: any) => {
console.log('handleEdit', record);
parentId.value = null;
isInterface.value = false;
currentRecord.value = filterRecord(record);
editModalVisible.value = true;
};
//
const handleEditInterface = (record: any) => {
console.log('handleEdit', record);
currentRecord.value = filterRecord(record);
console.log('handleEdit', currentRecord.value);
isInterface.value = true;
parentId.value = null;
editModalVisible.value = true;
};
//
const handleDelete = async (record: any) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的记录吗?',
zIndex: 2002,
onOk: async () => {
try {
let res = await deleteMapLayer([record.id]);
message.success('删除成功');
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');
return;
}
} }
if (
searchForm.messageTypeCode !== undefined &&
searchForm.messageTypeCode !== null
) {
filters.push({
field: 'messageTypeCode',
operator: 'eq',
dataType: 'string',
value: searchForm.messageTypeCode
}); });
}; }
// if (
const onSearchFinish = (values: any) => { searchForm.title !== undefined &&
searchParams.value = values; searchForm.title !== null &&
nextTick(() => { searchForm.title !== ''
basicTable.value.getList(); ) {
filters.push({
field: 'title',
operator: 'contains',
dataType: 'string',
value: searchForm.title
}); });
}; }
// if (searchForm.planTime && searchForm.planTime.length === 2) {
const handleReset = () => { filters.push({
searchParams.value = {}; field: 'planTime',
nextTick(() => { operator: 'gte',
basicTable.value.refresh(); dataType: 'string',
value: searchForm.planTime[0] + ' 00:00:00'
}); });
}; filters.push({
field: 'planTime',
// operator: 'lte',
const handleEnableChange = (record: any, checked: boolean) => { dataType: 'string',
currentRecord.value = filterRecord(record); value: searchForm.planTime[1] + ' 23:59:59'
console.log(currentRecord.value);
record.enable = checked ? 1 : 0;
handleEditSubmit({ enable: checked ? 1 : 0 }, 'switch');
};
//
const handleCheckableChange = (record: any, checked: boolean) => {
currentRecord.value = filterRecord(record);
record.checkable = checked ? 1 : 0;
handleEditSubmit({ checkable: checked ? 1 : 0 }, 'switch');
};
//
const editModalCancel = () => {
isInterface.value = false;
parentId.value = null;
currentRecord.value = null;
editModalVisible.value = false;
};
const handleEditSubmit = async (values: any, type: string) => {
editLoading.value = true;
try {
let res = await saveMapLayer({
...currentRecord.value,
...values,
parentId: parentId.value
}); });
if (res.code == 0) {
if (type === 'switch') {
message.success('修改状态成功');
} else {
message.success(`保存成功`);
} }
editModalVisible.value = false; const params = {
basicTable.value.refresh(); logic: 'and',
} else { filters
if (type === 'switch') { };
message.success('修改状态成功'); basicTable.value.getList(params);
} else { };
message.success(`保存成功`); const getDict = async () => {
} let res = await getDictItemsByCode({ dictCode: 'category' }); //
} let res1 = await getDictItemsByCode({ dictCode: 'push_status' }); //
} catch (error) { messageTypeCodeOption.value = res.data.map((item: any) => ({
if (type === 'switch') { value: item.itemCode,
message.error('修改状态失败'); label: item.dictName
} else { }));
message.error(`保存失败`); statusOption.value = res1.data.map((item: any) => ({
} value: item.itemCode,
} finally { label: item.dictName
editLoading.value = false; }));
};
//
const getname = (value: string, option: any) => {
return option.find((item: any) => item.value === value)?.label;
};
const showPushContent = (record: any) => {
pushContentData.type = record.type || '';
pushContentData.title = record.title || '';
pushContentData.content = record.content || '';
pushContentData.planTime = record.planTime || '';
pushContentData.source = record.source || '';
pushContentData.relation = record.relation || '';
pushContentVisible.value = true;
};
useDraggable(pushContentVisible, { boundary: true, resetOnOpen: true });
const recordArr:any = ref([])
const showPushStatus = async (record: any) => {
recordArr.value = record;
pushStatusVisible.value = true;
await nextTick();
await nextTick();
if (pushStatusModalRef.value) {
pushStatusModalRef.value.handleSearch();
} }
}; };
onMounted(async () => {
await getDict();
onSearchFinish();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -355,4 +302,23 @@ const handleEditSubmit = async (values: any, type: string) => {
width: 17px !important; width: 17px !important;
margin: 0 !important; margin: 0 !important;
} }
.push-content-detail {
.detail-item {
display: flex;
margin-bottom: 12px;
line-height: 1.6;
.label {
font-weight: 500;
min-width: 80px;
flex-shrink: 0;
}
.value {
flex: 1;
word-break: break-all;
&.content {
white-space: pre-wrap;
}
}
}
}
</style> </style>