推送管理
This commit is contained in:
commit
fce3a5907a
@ -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: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:
|
||||
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
||||
|
||||
100
frontend/src/api/system/map/pushManagement/index.ts
Normal file
100
frontend/src/api/system/map/pushManagement/index.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@ -79,21 +79,21 @@ const data = ref([
|
||||
name: '正常',
|
||||
num: '0',
|
||||
warnState: '1',
|
||||
icon: 'iconfont icon-shuidianzhan1 zhuangji',
|
||||
icon: 'iconfont icon-map-rgszzDabiao zhuangji',
|
||||
background: '#78c300',
|
||||
},
|
||||
{
|
||||
name: '告警',
|
||||
num: '0',
|
||||
warnState: '2',
|
||||
icon: 'iconfont icon-shuidianzhanGaojing shebeiZhengchang',
|
||||
icon: 'iconfont icon-map-rgszzDabiao shebeiZhengchang',
|
||||
background: '#e55555',
|
||||
},
|
||||
{
|
||||
name: '无数据',
|
||||
num: '0',
|
||||
warnState: '0',
|
||||
icon: 'iconfont icon-shuidianzhanWushujv shebeiLixian',
|
||||
icon: 'iconfont icon-map-rgszzDabiao shebeiLixian',
|
||||
background: '#8d8d8d',
|
||||
},
|
||||
]);
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<a-button type="link" @click="backToLogin" class="back-button">
|
||||
<span>← 返回</span>
|
||||
</a-button>
|
||||
<h3 class="forgot-password-title">忘记密码</h3>
|
||||
<!-- <h3 class="forgot-password-title">忘记密码</h3> -->
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
@ -32,7 +32,11 @@
|
||||
class="form-container"
|
||||
>
|
||||
<a-form-item label="" name="phone">
|
||||
<a-input placeholder="请输入手机号" size="large">
|
||||
<a-input
|
||||
v-model:value="forgotPasswordForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<MobileOutlined />
|
||||
</template>
|
||||
@ -42,9 +46,10 @@
|
||||
<a-form-item label="" name="captcha">
|
||||
<div class="captcha-row">
|
||||
<a-input
|
||||
v-model="forgotPasswordForm.captcha"
|
||||
v-model:value="forgotPasswordForm.captcha"
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
autocomplete="current-captcha"
|
||||
class="captcha-input"
|
||||
>
|
||||
<template #prefix>
|
||||
@ -69,7 +74,8 @@
|
||||
|
||||
<a-form-item label="" name="newPassword">
|
||||
<a-input-password
|
||||
v-model="forgotPasswordForm.newPassword"
|
||||
v-model:value="forgotPasswordForm.newPassword"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入新密码"
|
||||
size="large"
|
||||
>
|
||||
@ -79,6 +85,19 @@
|
||||
</a-input-password>
|
||||
</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
|
||||
type="primary"
|
||||
size="large"
|
||||
@ -162,32 +181,51 @@
|
||||
>
|
||||
<span>登录</span>
|
||||
</a-button>
|
||||
<!-- <a-button
|
||||
type="link"
|
||||
size="mini"
|
||||
block
|
||||
@click="showForgotPasswordPage"
|
||||
:style="{ marginTop: '10px', border: 'none' }"
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
"
|
||||
>
|
||||
忘记密码
|
||||
</a-button> -->
|
||||
<a-checkbox v-model:checked="remember" style="margin-top: 10px"
|
||||
>记住密码</a-checkbox
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
size="mini"
|
||||
@click="showForgotPasswordPage"
|
||||
:style="{ marginTop: '10px', border: 'none' }"
|
||||
>
|
||||
忘记密码
|
||||
</a-button>
|
||||
<!-- <a-button
|
||||
type="link"
|
||||
size="mini"
|
||||
@click="goRegister"
|
||||
:style="{ marginTop: '10px', border: 'none' }"
|
||||
>
|
||||
用户注册
|
||||
</a-button> -->
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码 -->
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 短信登录 Tab (占位) -->
|
||||
<!-- <a-tab-pane key="sms" tab="短信登录">
|
||||
<a-tab-pane key="sms" tab="短信登录">
|
||||
<a-form
|
||||
:model="loginData"
|
||||
:rules="loginRules"
|
||||
:rules="state.smsLoginRules"
|
||||
layout="vertical"
|
||||
class="form-container"
|
||||
@finish="onSmsLogin"
|
||||
>
|
||||
<a-form-item label="" name="username">
|
||||
<a-input
|
||||
v-model:value="loginData.username"
|
||||
autocomplete="current-username"
|
||||
placeholder="请输入手机号"
|
||||
size="large"
|
||||
>
|
||||
@ -199,9 +237,8 @@
|
||||
<a-form-item label="" name="captcha">
|
||||
<div class="captcha-row">
|
||||
<a-input
|
||||
v-model="loginData.code"
|
||||
v-model:value="loginData.code"
|
||||
placeholder="请输入验证码"
|
||||
autocomplete="current-captcha"
|
||||
size="large"
|
||||
class="captcha-input"
|
||||
>
|
||||
@ -235,8 +272,36 @@
|
||||
>
|
||||
<span>登录</span>
|
||||
</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-tab-pane> -->
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@ -248,19 +313,23 @@
|
||||
import { onMounted, reactive, ref, toRefs, watch } from "vue";
|
||||
import loginImg from "@/assets/images/logo.png";
|
||||
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 { setToken } from "@/utils/auth";
|
||||
import Cookies from "js-cookie";
|
||||
// 组件依赖
|
||||
|
||||
import router from "@/router";
|
||||
// API依赖
|
||||
import { useRoute } from "vue-router";
|
||||
import { LoginData } from "@/api/auth/types";
|
||||
|
||||
//密码加密
|
||||
import { encrypt } from "@/utils/rsaEncrypt";
|
||||
import { encrypt, decrypt } from "@/utils/rsaEncrypt";
|
||||
|
||||
// 状态管理依赖
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
// 国际化
|
||||
import { useI18n } from "vue-i18n";
|
||||
@ -280,6 +349,7 @@ const forgotPasswordForm = ref({
|
||||
phone: "",
|
||||
captcha: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
// 忘记密码
|
||||
const showForgotPassword = ref(false);
|
||||
@ -299,6 +369,10 @@ const state = reactive({
|
||||
password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }],
|
||||
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
|
||||
},
|
||||
smsLoginRules: {
|
||||
username: [{ required: true, trigger: "blur", message: "请输入手机号" }],
|
||||
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
|
||||
},
|
||||
loginImg: loginImg[0],
|
||||
loading: false,
|
||||
passwordType: "password",
|
||||
@ -320,15 +394,80 @@ const forgotPasswordRules = ref({
|
||||
captcha: [{ required: true, message: "验证码不能为空", trigger: "blur" }],
|
||||
newPassword: [
|
||||
{ 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,
|
||||
// passwordType, capslockTooltipDisabled
|
||||
} = toRefs(
|
||||
state
|
||||
);
|
||||
const {
|
||||
loginData,
|
||||
loginRules,
|
||||
loading,
|
||||
// passwordType, capslockTooltipDisabled
|
||||
} = toRefs(state);
|
||||
|
||||
// function checkCapslock(e: any) {
|
||||
// const { key } = e;
|
||||
@ -364,8 +503,19 @@ function onFinish() {
|
||||
userStore
|
||||
.login(user)
|
||||
.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: "/" });
|
||||
state.loading = false;
|
||||
message.success("登录成功");
|
||||
})
|
||||
.catch(() => {
|
||||
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(
|
||||
route,
|
||||
() => {
|
||||
@ -395,6 +632,22 @@ function getOtherQuery(query: any) {
|
||||
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() {
|
||||
getCaptcha().then((result: any) => {
|
||||
codeUrl.value = result.data.img;
|
||||
@ -420,7 +673,10 @@ const startCountdown = () => {
|
||||
const showForgotPasswordPage = () => {
|
||||
showForgotPassword.value = true;
|
||||
};
|
||||
|
||||
//注册用户页面
|
||||
const goRegister = () => {
|
||||
router.push({ path: "/register" });
|
||||
};
|
||||
// 返回登录页面
|
||||
const backToLogin = () => {
|
||||
showForgotPassword.value = false;
|
||||
@ -429,6 +685,7 @@ const backToLogin = () => {
|
||||
phone: "",
|
||||
captcha: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
};
|
||||
|
||||
@ -456,10 +713,13 @@ const sendSms = async () => {
|
||||
smsButtonDisabled.value = true;
|
||||
|
||||
try {
|
||||
// 模拟发送短信验证码接口
|
||||
// await axios.post('/sms/send', { phone: loginForm.value.username })
|
||||
|
||||
// 模拟发送成功
|
||||
// 调用发送短信验证码接口,type默认为1(注册类型)
|
||||
const res: any = await sendSmsCode(loginData.value.username, 3);
|
||||
if (res.code == 1) {
|
||||
message.success(res.msg);
|
||||
return;
|
||||
}
|
||||
// 发送成功
|
||||
message.success("验证码发送成功");
|
||||
|
||||
// 开始倒计时
|
||||
@ -496,17 +756,23 @@ const sendForgotPasswordSms = async () => {
|
||||
smsButtonDisabled.value = true;
|
||||
|
||||
try {
|
||||
// 模拟发送短信验证码接口
|
||||
// await axios.post('/sms/forgot-password', { phone: forgotPasswordForm.value.phone })
|
||||
// 调用发送短信验证码接口,type=3表示找回密码类型
|
||||
const res: any = await sendSmsCode(forgotPasswordForm.value.phone, 2);
|
||||
|
||||
// 模拟发送成功
|
||||
if (res.code == 1) {
|
||||
message.error(res.msg);
|
||||
smsButtonDisabled.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送成功
|
||||
message.success("验证码发送成功");
|
||||
|
||||
// 开始倒计时
|
||||
startCountdown();
|
||||
} catch (error) {
|
||||
console.error("发送验证码失败", error);
|
||||
message.error("验证码发送失败,请重试");
|
||||
} catch (error: any) {
|
||||
// console.error("发送验证码失败", error);
|
||||
// message.error(error.message || "验证码发送失败,请重试");
|
||||
smsButtonDisabled.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@ -514,23 +780,117 @@ const sendForgotPasswordSms = async () => {
|
||||
};
|
||||
// 处理密码重置
|
||||
const handleResetPassword = async () => {
|
||||
loading.value = true;
|
||||
// 先进行表单验证
|
||||
try {
|
||||
// 2. 模拟接口请求 (实际替换为真实重置密码接口)
|
||||
// await axios.post('/user/reset-password', {
|
||||
// phone: forgotPasswordForm.value.phone,
|
||||
// captcha: forgotPasswordForm.value.captcha,
|
||||
// newPassword: forgotPasswordForm.value.newPassword
|
||||
// })
|
||||
// 手动验证各个字段
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (
|
||||
!forgotPasswordForm.value.phone ||
|
||||
!phoneRegex.test(forgotPasswordForm.value.phone)
|
||||
) {
|
||||
message.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 模拟重置成功
|
||||
message.success("密码重置成功");
|
||||
if (!forgotPasswordForm.value.captcha) {
|
||||
message.error("请输入验证码");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 返回登录页面
|
||||
backToLogin();
|
||||
if (!forgotPasswordForm.value.newPassword) {
|
||||
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) {
|
||||
console.error("密码重置失败", error);
|
||||
message.error("密码重置失败,请重试");
|
||||
console.error("表单验证失败", 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 {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -538,6 +898,7 @@ const handleResetPassword = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
getCode();
|
||||
getCookie();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -592,7 +953,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
position: relative;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 72%;
|
||||
min-height: 600px;
|
||||
@ -603,7 +964,7 @@ onMounted(() => {
|
||||
|
||||
// 左侧背景区域
|
||||
.left-section {
|
||||
.slogan {
|
||||
.slogan {
|
||||
position: absolute;
|
||||
top: 28%;
|
||||
left: 18%;
|
||||
@ -635,10 +996,12 @@ onMounted(() => {
|
||||
/* 验证码布局 */
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
|
||||
.ant-row {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* gap: 12px;
|
||||
align-items: center; */
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="isEdit ? '编辑沿程配置' : '新增沿程配置'"
|
||||
:title="isEdit ? '修改编辑推送类型' : '新增编辑推送类型'"
|
||||
v-model:open="modalVisible"
|
||||
:confirm-loading="localLoading"
|
||||
width="1200px"
|
||||
width="35vw"
|
||||
:destroy-on-close="true"
|
||||
ok-text="保存"
|
||||
@cancel="handleCancel"
|
||||
@ -14,279 +14,105 @@
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
name="edit_form"
|
||||
:labelCol="{ span: 5 }"
|
||||
:labelCol="{ span: 4 }"
|
||||
>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="沿程配置名称" name="name">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="类型名称" name="messageType">
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
v-model:value="formData.messageType"
|
||||
style="width: 100%"
|
||||
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="请输入配置编码"
|
||||
placeholder="请输入类型名称"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所在河段" name="rvnm">
|
||||
<a-input
|
||||
v-model:value="formData.rvnm"
|
||||
<a-col :span="24">
|
||||
<a-form-item label="所属类别" name="categoryCode">
|
||||
<a-select
|
||||
v-model:value="formData.categoryCode"
|
||||
placeholder="请选择"
|
||||
style="width: 100%"
|
||||
placeholder="请输入所在河段"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="备注" name="description">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
v-model:value="formData.description"
|
||||
style="width: 100%"
|
||||
placeholder="请输入备注"
|
||||
/> </a-form-item
|
||||
:options="categoryCodeOption"
|
||||
@change="handleCategoryChange"
|
||||
></a-select> </a-form-item
|
||||
></a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<div class="config-tree-container">
|
||||
<!-- 新增一级节点区域 -->
|
||||
<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-tree
|
||||
v-if="treeData.length > 0"
|
||||
:tree-data="treeData"
|
||||
:default-expand-all="false"
|
||||
:selectable="false"
|
||||
block-node
|
||||
style="width: 300px"
|
||||
>
|
||||
<template #title="{ dataRef }">
|
||||
<!-- 编辑状态:只显示编辑区域 -->
|
||||
<div
|
||||
v-if="editingNode && editingNodeKey === dataRef.key"
|
||||
class="add-child-node-row w-[300px]"
|
||||
>
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="关联基地">
|
||||
<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>
|
||||
<!-- 子节点添加区域 -->
|
||||
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
|
||||
v-if="addingChildNode && currentParentKey === dataRef.key"
|
||||
class="add-child-node-row"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="childNodeData.stcd"
|
||||
show-search
|
||||
placeholder="请选择站点"
|
||||
style="width: 300px"
|
||||
:filter-option="filterOption"
|
||||
@change="handleChildNodeSelect"
|
||||
<a-input
|
||||
v-model:value="formData.cronExpression"
|
||||
style="width: 70%"
|
||||
placeholder="请输入cron表达式"
|
||||
/>
|
||||
<a-button style="width: 27%" @click="openCronModal"
|
||||
>cron表达式</a-button
|
||||
>
|
||||
<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-form-item></a-col
|
||||
>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
title="Cron表达式选择器"
|
||||
v-model:open="cronModalVisible"
|
||||
width="1200px"
|
||||
:destroy-on-close="true"
|
||||
footer=""
|
||||
@cancel="closeCronModal"
|
||||
>
|
||||
<CronPicker
|
||||
v-model="formData.cronExpression"
|
||||
@confirm="handleCronConfirm"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<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 type { Rule } from 'ant-design-vue/es/form';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
PlusOutlined,
|
||||
MinusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
import CronPicker from './CronPicker.vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
initialValues?: any | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const categoryCodeOption:any =ref([])
|
||||
const localLoading = ref(false);
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
@ -309,368 +135,82 @@ const formRef = ref();
|
||||
|
||||
const defaultFormData = reactive({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
rvnm: undefined,
|
||||
description: undefined
|
||||
messageType: undefined,
|
||||
categoryCode: undefined,
|
||||
categoryName: undefined,
|
||||
baseIdList: [],
|
||||
baseNameList: [],
|
||||
cronExpression: undefined
|
||||
});
|
||||
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[]> = {
|
||||
name: [{ required: true, message: '请输入沿程配置名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入配置编码', trigger: 'blur' }],
|
||||
rvnm: [{ required: true, message: '请输入所在河段', trigger: 'blur' }]
|
||||
messageType: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
|
||||
categoryCode: [{ required: true, message: '请选择所属类别', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
const isEdit = computed(() => !!props.initialValues);
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref<any[]>([]);
|
||||
|
||||
// 新增节点相关
|
||||
const addingNewNode = ref(false);
|
||||
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 cronModalVisible = ref(false);
|
||||
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(cronModalVisible, { boundary: true, resetOnOpen: true });
|
||||
const openCronModal = () => {
|
||||
cronModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 搜索过滤
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return (
|
||||
option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
);
|
||||
const closeCronModal = () => {
|
||||
cronModalVisible.value = false;
|
||||
};
|
||||
|
||||
// 判断是否为第一个节点
|
||||
const isFirstNode = (dataRef: any) => {
|
||||
const siblings = getSiblings(dataRef);
|
||||
return siblings.length > 0 && siblings[0].key === dataRef.key;
|
||||
const handleCronConfirm = (value: string) => {
|
||||
formData.cronExpression = value;
|
||||
closeCronModal();
|
||||
};
|
||||
|
||||
// 判断是否为最后一个节点
|
||||
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 () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
const submitValues = {
|
||||
...formData,
|
||||
captureDate: formData.captureDate
|
||||
? dayjs(formData.captureDate).format('YYYY-MM-DD')
|
||||
: undefined,
|
||||
configNodes: treeData.value // 提交树形数据
|
||||
};
|
||||
emit('ok', submitValues);
|
||||
|
||||
const submitData: any = { ...formData };
|
||||
|
||||
if ((formData.baseIdList || []).includes('all')) {
|
||||
const allCodes = JidiSelectEventStore.jidiData
|
||||
.filter(item => item.wbsCode !== 'all')
|
||||
.map(item => item.wbsCode);
|
||||
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) {
|
||||
console.error('Validate Failed:', error);
|
||||
message.error('请检查表单填写是否正确');
|
||||
@ -678,32 +218,46 @@ const handleOk = async () => {
|
||||
};
|
||||
|
||||
const initForm = () => {
|
||||
resetForm();
|
||||
if (props.initialValues) {
|
||||
const values = props.initialValues;
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (values.hasOwnProperty(key)) {
|
||||
if (key === 'captureDate' && values[key]) {
|
||||
formData[key] = dayjs(values[key]);
|
||||
} else if (key === 'baseIdList' || key === 'baseNameList') {
|
||||
formData[key] = values[key] || [];
|
||||
} else {
|
||||
formData[key] = values[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化树形数据
|
||||
if (values.configNodes && Array.isArray(values.configNodes)) {
|
||||
treeData.value = values.configNodes.map((node: any) => ({
|
||||
...node,
|
||||
children:
|
||||
node.children?.map((child: any) => ({
|
||||
...child
|
||||
})) || []
|
||||
}));
|
||||
} else {
|
||||
treeData.value = [];
|
||||
|
||||
if (values.baseIdList) {
|
||||
const baseIds = typeof values.baseIdList === 'string'
|
||||
? values.baseIdList.split(',')
|
||||
: (Array.isArray(values.baseIdList) ? values.baseIdList : []);
|
||||
const allBaseCodes = JidiSelectEventStore.jidiData
|
||||
.filter(item => item.wbsCode !== 'all')
|
||||
.map(item => item.wbsCode);
|
||||
const isAllSelected = allBaseCodes.every(code => baseIds.includes(code));
|
||||
|
||||
if (isAllSelected) {
|
||||
formData.baseIdList = ['all'];
|
||||
formData.baseNameList = ['当前全部'];
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
Object.assign(formData, defaultFormData);
|
||||
treeData.value = [];
|
||||
addingNewNode.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -732,6 +284,17 @@ watch(
|
||||
{ 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({
|
||||
localLoading
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:showSearch="false"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
@ -38,22 +39,22 @@ const initSearchData = {
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: "Input",
|
||||
name: "name",
|
||||
label: "沿程配置名称",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "Input",
|
||||
name: "rvnm",
|
||||
label: "所在河段",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
// {
|
||||
// type: "Input",
|
||||
// name: "name",
|
||||
// label: "沿程配置名称",
|
||||
// fieldProps: {
|
||||
// allowClear: true,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: "Input",
|
||||
// name: "rvnm",
|
||||
// label: "所在河段",
|
||||
// fieldProps: {
|
||||
// allowClear: true,
|
||||
// },
|
||||
// },
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -7,15 +7,30 @@
|
||||
:handle-add="handleAdd"
|
||||
@reset="handleReset"
|
||||
@search-finish="onSearchFinish"
|
||||
/>
|
||||
/> -->
|
||||
<div><a-button type="primary" @click="handleAdd">新增</a-button></div>
|
||||
<BasicTable
|
||||
ref="basicTable"
|
||||
:columns="columns"
|
||||
:list-url="getAllConfigTree"
|
||||
:list-url="getPushConfigList"
|
||||
: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 }">
|
||||
<div class="flex gap-[6px]">
|
||||
<a-button
|
||||
class="!p-0"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handlePushTarget(record)"
|
||||
>推送目标</a-button
|
||||
>
|
||||
<a-button
|
||||
class="!p-0"
|
||||
type="link"
|
||||
@ -40,79 +55,61 @@
|
||||
:initial-values="currentRecord"
|
||||
@cancel="editModalCancel"
|
||||
@ok="handleEditSubmit"
|
||||
/> -->
|
||||
/>
|
||||
<PushTargetModal
|
||||
ref="pushTargetModal"
|
||||
v-model:visible="pushTargetModalVisible"
|
||||
:initial-values="pushTargetInitialValues"
|
||||
@cancel="pushTargetModalCancel"
|
||||
@ok="handlePushTargetSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import ConfigManagementSearch from './ConfigManagementSearch.vue';
|
||||
import ConfigManagementForm from './ConfigManagementForm.vue';
|
||||
import PushTargetModal from './PushTargetModal.vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
getAllConfigTree,
|
||||
deleteBaseWbsb,
|
||||
saveBaseWbsb
|
||||
} from '@/api/system/map/ConfigManagement';
|
||||
getPushConfigList,
|
||||
addPushConfig,
|
||||
updatePushConfig,
|
||||
deletePushConfig,
|
||||
savePushTargets,
|
||||
setPushConfigSmsSwitch
|
||||
} from '@/api/system/map/pushManagement';
|
||||
|
||||
// 表格实例
|
||||
const basicTable = ref<any>(null);
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
key: 'index',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
customRender: ({ text, record, index }) => index + 1
|
||||
title: '消息类型',
|
||||
dataIndex: 'messageType',
|
||||
key: 'messageType'
|
||||
},
|
||||
{
|
||||
title: '沿程配置名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
title: '当日推送次数',
|
||||
key: 'dailyPushCount',
|
||||
dataIndex: 'dailyPushCount'
|
||||
},
|
||||
{
|
||||
title: '配置编码',
|
||||
key: 'code',
|
||||
dataIndex: 'code',
|
||||
width: 120
|
||||
title: '累计推送次数',
|
||||
dataIndex: 'pushCount',
|
||||
key: 'pushCount'
|
||||
},
|
||||
{
|
||||
title: '所在河段',
|
||||
dataIndex: 'rvnm',
|
||||
key: 'rvnm',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
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: '短信提醒',
|
||||
key: 'isSms',
|
||||
dataIndex: 'isSms',
|
||||
slots: { customRender: 'isSms' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
@ -124,6 +121,11 @@ const searchParams = ref({});
|
||||
const currentRecord = ref<any | null>(null);
|
||||
const editModalVisible = ref(false);
|
||||
|
||||
// 推送目标弹窗
|
||||
const pushTargetModal = ref<any>(null);
|
||||
const pushTargetModalVisible = ref(false);
|
||||
const pushTargetInitialValues = ref<any | null>(null);
|
||||
|
||||
// 新增处理
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null;
|
||||
@ -136,6 +138,33 @@ const handleEdit = (record: any) => {
|
||||
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) => {
|
||||
Modal.confirm({
|
||||
@ -144,11 +173,13 @@ const handleDelete = (record: any) => {
|
||||
zIndex: 2002,
|
||||
onOk: async () => {
|
||||
try {
|
||||
let res = await deleteBaseWbsb([record.id]);
|
||||
message.success('删除成功');
|
||||
let res = await deletePushConfig([record.id]);
|
||||
if ((res.code = 0)) {
|
||||
message.success('删除成功');
|
||||
}
|
||||
basicTable.value.refresh();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
// message.error('删除失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -156,39 +187,13 @@ const handleDelete = (record: any) => {
|
||||
};
|
||||
|
||||
// 搜索完成处理
|
||||
const onSearchFinish = (values: any) => {
|
||||
const onSearchFinish = () => {
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
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)
|
||||
filters: [].filter(Boolean)
|
||||
};
|
||||
basicTable.value.getList(params);
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
basicTable.value.getList({
|
||||
logic: 'and',
|
||||
filters: []
|
||||
});
|
||||
};
|
||||
|
||||
// 表单取消
|
||||
const editModalCancel = () => {
|
||||
currentRecord.value = null;
|
||||
@ -198,25 +203,55 @@ const editModalCancel = () => {
|
||||
// 表单提交
|
||||
const handleEditSubmit = async (values: any) => {
|
||||
try {
|
||||
let res = await saveBaseWbsb({
|
||||
...currentRecord.value,
|
||||
...values
|
||||
});
|
||||
message.success(`保存成功`);
|
||||
//updatePushConfig
|
||||
if (values.id) {
|
||||
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(`保存成功`);
|
||||
}
|
||||
}
|
||||
|
||||
editModalVisible.value = false;
|
||||
basicTable.value.refresh();
|
||||
} catch (error) {
|
||||
message.error(`保存失败`);
|
||||
// message.error(`保存失败`);
|
||||
|
||||
editModalVisible.value = false;
|
||||
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(() => {
|
||||
initOption();
|
||||
onSearchFinish();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -1,208 +1,161 @@
|
||||
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LayerManagement\index.vue -->
|
||||
<template>
|
||||
<div class="content">
|
||||
推送历史
|
||||
<!-- <LayerManagementSearch
|
||||
ref="layerManagementSearch"
|
||||
:handle-add="handleAdd"
|
||||
@reset="handleReset"
|
||||
@search-finish="onSearchFinish"
|
||||
/>
|
||||
<!-- 推送历史 -->
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item label="">
|
||||
<a-input
|
||||
v-model:value="searchForm.title"
|
||||
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
|
||||
v-if="statusOption.length > 0"
|
||||
ref="basicTable"
|
||||
:columns="columns"
|
||||
:list-url="getAllMapLayerTree"
|
||||
:list-url="postPushHistoryList"
|
||||
: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 }">
|
||||
<div class="flex gap-[6px]">
|
||||
<a-button
|
||||
class="!p-0"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
@click="showPushContent(record)"
|
||||
>推送内容</a-button
|
||||
>
|
||||
<a-button
|
||||
v-if="record.children && record.children.length == 0"
|
||||
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
|
||||
<a-button class="!p-0" type="link" size="small" @click="showPushStatus(record)"
|
||||
>推送情况</a-button
|
||||
>
|
||||
</div>
|
||||
</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>
|
||||
<LayerManagementForm
|
||||
ref="layerManagementForm"
|
||||
v-model:visible="editModalVisible"
|
||||
:initial-values="currentRecord"
|
||||
:is-interface="isInterface"
|
||||
:loading="editLoading"
|
||||
@cancel="editModalCancel"
|
||||
@ok="handleEditSubmit"
|
||||
/> -->
|
||||
|
||||
<a-modal
|
||||
v-model:open="pushContentVisible"
|
||||
title="推送内容"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import LayerManagementSearch from './LayerManagementSearch.vue';
|
||||
import LayerManagementForm from './LayerManagementForm.vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
getAllMapLayerTree,
|
||||
saveMapLayer,
|
||||
deleteMapLayer
|
||||
} from '@/api/system/map/LayerManagement';
|
||||
// 表格列配置
|
||||
import { postPushHistoryList } from '@/api/system/map/pushManagement';
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
import PushStatusModal from './PushStatusModal/index.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: '',
|
||||
width: 50,
|
||||
customRender: () => {
|
||||
return '';
|
||||
}
|
||||
title: '生成时间',
|
||||
dataIndex: 'planTime',
|
||||
key: 'planTime'
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 60,
|
||||
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: '消息类型',
|
||||
dataIndex: 'messageType',
|
||||
key: 'messageType'
|
||||
},
|
||||
{
|
||||
title: '图层名称',
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
width: 150
|
||||
key: 'title'
|
||||
},
|
||||
|
||||
{
|
||||
title: '已推送/成功/待推送/失败',
|
||||
dataIndex: 'number',
|
||||
key: 'number',
|
||||
slots: { customRender: 'number' }
|
||||
},
|
||||
{
|
||||
title: '图层编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
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: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
slots: { customRender: 'status' }
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
@ -211,139 +164,133 @@ const columns = [
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
];
|
||||
// 搜索参数
|
||||
// 表格实例
|
||||
const basicTable = ref<any>(null);
|
||||
// 搜索参数
|
||||
const searchParams = ref({});
|
||||
// 编辑弹窗数据
|
||||
const isInterface = ref(false);
|
||||
const editModalVisible = ref(false);
|
||||
const editLoading = ref(false);
|
||||
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 pushStatusModalRef = ref<any>(null);
|
||||
const searchParams = ref({
|
||||
sort: [
|
||||
{
|
||||
field: 'createdAt',
|
||||
dir: 'desc'
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
]
|
||||
});
|
||||
// 搜索表单
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// 搜索
|
||||
const onSearchFinish = (values: any) => {
|
||||
searchParams.value = values;
|
||||
nextTick(() => {
|
||||
basicTable.value.getList();
|
||||
});
|
||||
};
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.value = {};
|
||||
nextTick(() => {
|
||||
basicTable.value.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
// 是否启用切换处理
|
||||
const handleEnableChange = (record: any, checked: boolean) => {
|
||||
currentRecord.value = filterRecord(record);
|
||||
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
|
||||
const onSearchFinish = () => {
|
||||
const filters = [];
|
||||
if (searchForm.status !== undefined && searchForm.status !== null) {
|
||||
filters.push({
|
||||
field: 'status',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: searchForm.status
|
||||
});
|
||||
if (res.code == 0) {
|
||||
if (type === 'switch') {
|
||||
message.success('修改状态成功');
|
||||
} else {
|
||||
message.success(`保存成功`);
|
||||
}
|
||||
editModalVisible.value = false;
|
||||
basicTable.value.refresh();
|
||||
} else {
|
||||
if (type === 'switch') {
|
||||
message.success('修改状态成功');
|
||||
} else {
|
||||
message.success(`保存成功`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (type === 'switch') {
|
||||
message.error('修改状态失败');
|
||||
} else {
|
||||
message.error(`保存失败`);
|
||||
}
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
if (
|
||||
searchForm.messageTypeCode !== undefined &&
|
||||
searchForm.messageTypeCode !== null
|
||||
) {
|
||||
filters.push({
|
||||
field: 'messageTypeCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: searchForm.messageTypeCode
|
||||
});
|
||||
}
|
||||
if (
|
||||
searchForm.title !== undefined &&
|
||||
searchForm.title !== null &&
|
||||
searchForm.title !== ''
|
||||
) {
|
||||
filters.push({
|
||||
field: 'title',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: searchForm.title
|
||||
});
|
||||
}
|
||||
if (searchForm.planTime && searchForm.planTime.length === 2) {
|
||||
filters.push({
|
||||
field: 'planTime',
|
||||
operator: 'gte',
|
||||
dataType: 'string',
|
||||
value: searchForm.planTime[0] + ' 00:00:00'
|
||||
});
|
||||
filters.push({
|
||||
field: 'planTime',
|
||||
operator: 'lte',
|
||||
dataType: 'string',
|
||||
value: searchForm.planTime[1] + ' 23:59:59'
|
||||
});
|
||||
}
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
basicTable.value.getList(params);
|
||||
};
|
||||
const getDict = async () => {
|
||||
let res = await getDictItemsByCode({ dictCode: 'category' }); //全部消息类型
|
||||
let res1 = await getDictItemsByCode({ dictCode: 'push_status' }); //全部状态
|
||||
messageTypeCodeOption.value = res.data.map((item: any) => ({
|
||||
value: item.itemCode,
|
||||
label: item.dictName
|
||||
}));
|
||||
statusOption.value = res1.data.map((item: any) => ({
|
||||
value: item.itemCode,
|
||||
label: item.dictName
|
||||
}));
|
||||
};
|
||||
//字典转换
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -355,4 +302,23 @@ const handleEditSubmit = async (values: any, type: string) => {
|
||||
width: 17px !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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user