更新
This commit is contained in:
parent
f9db706769
commit
90b290af1f
@ -5,11 +5,25 @@ export function login(data) {
|
|||||||
return request({
|
return request({
|
||||||
url: `/admin/login`,
|
url: `/admin/login`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data,
|
||||||
tenant_name: data.tenant_name,
|
});
|
||||||
account: data.account,
|
}
|
||||||
password: data.password,
|
|
||||||
},
|
// 发送登录验证码(手机号)
|
||||||
|
export function sendLoginCode(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sendLoginCode",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号验证码登录
|
||||||
|
export function loginBySms(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/loginBySms",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 登出
|
// 登出
|
||||||
@ -64,4 +78,40 @@ export function getOpenVerify() {
|
|||||||
url: '/admin/login/getOpenVerify',
|
url: '/admin/login/getOpenVerify',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
export function register(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/register",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送注册验证码
|
||||||
|
export function sendRegisterCode(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sendRegisterCode",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 忘记密码重置
|
||||||
|
export function resetPassword(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/resetPassword",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送找回密码验证码
|
||||||
|
export function sendResetCode(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sendResetCode",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
56
src/api/sms.ts
Normal file
56
src/api/sms.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信网关配置
|
||||||
|
*/
|
||||||
|
export function getSmsInfo() {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sms/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑短信网关配置
|
||||||
|
*/
|
||||||
|
export function editSmsInfo(data: any) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sms/editinfo",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送测试短信(入队任务,等待网关发送)
|
||||||
|
*/
|
||||||
|
export function sendTestSms(data: any) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sms/sendtest",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信任务列表(租户隔离)
|
||||||
|
*/
|
||||||
|
export function getSmsTaskList(params: { status?: string | number; phone?: string } = {}) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/sms/taskList",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑短信任务
|
||||||
|
*/
|
||||||
|
export function editSmsTask(id: number | string, data: any) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/sms/taskEdit/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -24,6 +24,18 @@ const staticRoutes = [
|
|||||||
component: () => import("@/views/login/index.vue"),
|
component: () => import("@/views/login/index.vue"),
|
||||||
meta: { requiresAuth: false }
|
meta: { requiresAuth: false }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
name: "Register",
|
||||||
|
component: () => import("@/views/login/register.vue"),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/forget",
|
||||||
|
name: "ForgetPassword",
|
||||||
|
component: () => import("@/views/login/forget.vue"),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/home",
|
path: "/home",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
@ -151,8 +163,9 @@ function findFirstValidRoute(routes) {
|
|||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
const publicPaths = ["/login", "/register", "/forget"];
|
||||||
|
|
||||||
if (to.path === "/login") {
|
if (publicPaths.includes(to.path)) {
|
||||||
if (token) {
|
if (token) {
|
||||||
if (!dynamicRoutesAdded) {
|
if (!dynamicRoutesAdded) {
|
||||||
await loadAndAddDynamicRoutes();
|
await loadAndAddDynamicRoutes();
|
||||||
|
|||||||
@ -96,8 +96,8 @@
|
|||||||
import { ref, reactive, watch, nextTick } from 'vue';
|
import { ref, reactive, watch, nextTick } from 'vue';
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
import { Plus, Picture } from '@element-plus/icons-vue';
|
import { Plus, Picture } from '@element-plus/icons-vue';
|
||||||
import { createBaby, editBaby } from '@/api/babyhealth';
|
// import { createBaby, editBaby } from '@/api/babyhealth';
|
||||||
import { uploadAvatar } from '@/api/file';
|
// import { uploadAvatar } from '@/api/file';
|
||||||
|
|
||||||
import ImgCutter from 'vue-img-cutter';
|
import ImgCutter from 'vue-img-cutter';
|
||||||
|
|
||||||
|
|||||||
@ -87,8 +87,8 @@ import { ref, computed, watch } from "vue";
|
|||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { Plus } from '@element-plus/icons-vue';
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
||||||
import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
|
// import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
|
||||||
import { uploadAvatar } from '@/api/file';
|
// import { uploadAvatar } from '@/api/file';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@ -205,7 +205,7 @@ const cateOptions = ref([]);
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
title: "",
|
title: "",
|
||||||
author: "云泽网",
|
author: "",
|
||||||
cate: "",
|
cate: "",
|
||||||
content: "",
|
content: "",
|
||||||
image: "",
|
image: "",
|
||||||
@ -503,7 +503,7 @@ function resetForm() {
|
|||||||
// 重置表单数据
|
// 重置表单数据
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
title: "",
|
title: "",
|
||||||
author: "云泽网",
|
author: "",
|
||||||
cate: "",
|
cate: "",
|
||||||
content: "",
|
content: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
|||||||
@ -20,6 +20,16 @@
|
|||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="租户筛选">
|
||||||
|
<el-select v-model="selectedTenantId" style="width: 220px" disabled>
|
||||||
|
<el-option :label="`当前租户(ID: ${selectedTenantId || '-'})`" :value="selectedTenantId" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户列表表格 -->
|
<!-- 用户列表表格 -->
|
||||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
||||||
@ -84,14 +94,16 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||||
import { getAllUsers, deleteUser } from "@/api/user";
|
import { getTenantUsers, deleteUser } from "@/api/user";
|
||||||
import { getAllRoles } from "@/api/role";
|
import { getAllRoles } from "@/api/role";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import UserEditDialog from "./components/userEdit.vue";
|
import UserEditDialog from "./components/userEdit.vue";
|
||||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||||
import PreviewDialog from "./components/preview.vue";
|
import PreviewDialog from "./components/preview.vue";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
tid?: number;
|
||||||
account: string;
|
account: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
@ -121,6 +133,8 @@ const total = ref(0);
|
|||||||
const users = ref<User[]>([]);
|
const users = ref<User[]>([]);
|
||||||
const roles = ref<Role[]>([]);
|
const roles = ref<Role[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const selectedTenantId = ref<number>(Number(authStore.user.tid || 0));
|
||||||
|
|
||||||
// 组件引用
|
// 组件引用
|
||||||
const userEditRef = ref();
|
const userEditRef = ref();
|
||||||
@ -174,10 +188,17 @@ function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
|
|||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getAllUsers();
|
if (!selectedTenantId.value) {
|
||||||
users.value = res.data.list;
|
users.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await getTenantUsers(selectedTenantId.value);
|
||||||
|
users.value = res?.data?.list || [];
|
||||||
|
total.value = Number(res?.data?.total || 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
users.value = [];
|
users.value = [];
|
||||||
|
total.value = 0;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -254,12 +275,17 @@ const handleDelete = async (user: User) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
selectedTenantId.value = Number(authStore.user.tid || 0);
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.filter-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
127
src/views/login/forget.vue
Normal file
127
src/views/login/forget.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="card">
|
||||||
|
<h2>忘记密码</h2>
|
||||||
|
<p class="desc">通过租户、账号和手机号重置密码</p>
|
||||||
|
|
||||||
|
<input v-model="form.tenant_name" class="input" placeholder="租户名称" />
|
||||||
|
<input v-model="form.account" class="input" placeholder="账号" />
|
||||||
|
<input v-model="form.phone" class="input" placeholder="手机号" />
|
||||||
|
<div class="code-row">
|
||||||
|
<input v-model="form.sms_code" class="input code-input" placeholder="短信验证码" />
|
||||||
|
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendCode">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input v-model="form.new_password" class="input" type="password" placeholder="新密码" />
|
||||||
|
<input v-model="form.confirm_password" class="input" type="password" placeholder="确认新密码" />
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
|
||||||
|
<button class="btn" :disabled="loading" @click="handleSubmit">
|
||||||
|
{{ loading ? "提交中..." : "重置密码" }}
|
||||||
|
</button>
|
||||||
|
<a class="back" @click.prevent="router.push('/login')">返回登录</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onUnmounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { resetPassword, sendResetCode } from "@/api/login";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
const errorMsg = ref("");
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
tenant_name: "",
|
||||||
|
account: "",
|
||||||
|
phone: "",
|
||||||
|
sms_code: "",
|
||||||
|
new_password: "",
|
||||||
|
confirm_password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (codeLoading.value || countdown.value > 0) return;
|
||||||
|
if (!form.tenant_name || !form.account || !form.phone) {
|
||||||
|
errorMsg.value = "请先填写租户名称、账号和手机号";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
codeLoading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await sendResetCode({
|
||||||
|
tenant_name: form.tenant_name,
|
||||||
|
account: form.account,
|
||||||
|
phone: form.phone,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("验证码已发送");
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "验证码发送失败";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMsg.value = "验证码发送失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
codeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
errorMsg.value = "";
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await resetPassword(form);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("密码重置成功,请重新登录");
|
||||||
|
router.push("/login");
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "重置失败";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMsg.value = "重置失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f4f8ff; }
|
||||||
|
.card { width: 420px; background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 8px 24px rgba(0,0,0,.08); }
|
||||||
|
.desc { color: #6b7b93; margin-bottom: 12px; }
|
||||||
|
.input { width: 100%; margin-bottom: 10px; padding: 10px; border: 1px solid #dbe3ee; border-radius: 6px; box-sizing: border-box; }
|
||||||
|
.code-row { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.code-input { margin-bottom: 0; flex: 1; }
|
||||||
|
.code-btn { width: 120px; border: 1px solid #dbe3ee; border-radius: 6px; background: #fff; cursor: pointer; }
|
||||||
|
.code-btn:disabled { opacity: .6; cursor: not-allowed; }
|
||||||
|
.btn { width: 100%; margin-top: 8px; padding: 10px; border: none; border-radius: 6px; background: #3f8cff; color: #fff; cursor: pointer; }
|
||||||
|
.btn:disabled { opacity: .65; cursor: not-allowed; }
|
||||||
|
.error { color: #e34d4d; font-size: 13px; margin: 6px 0; }
|
||||||
|
.back { display: inline-block; margin-top: 10px; color: #3f8cff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -20,6 +20,10 @@
|
|||||||
<div class="login-panel">
|
<div class="login-panel">
|
||||||
<h2 class="login-title">欢迎登录</h2>
|
<h2 class="login-title">欢迎登录</h2>
|
||||||
<div class="login-desc">请填写您的账号信息</div>
|
<div class="login-desc">请填写您的账号信息</div>
|
||||||
|
<div class="mode-switch">
|
||||||
|
<button class="mode-btn" :class="{ active: loginMode === 'password' }" @click="switchMode('password')">账号密码登录</button>
|
||||||
|
<button class="mode-btn" :class="{ active: loginMode === 'sms' }" @click="switchMode('sms')">手机号验证码登录</button>
|
||||||
|
</div>
|
||||||
<div class="form-group icon-input-group">
|
<div class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-building-columns"></i>
|
<i class="fa-solid fa-building-columns"></i>
|
||||||
@ -27,13 +31,13 @@
|
|||||||
<input v-model="tenant_name" type="text" placeholder="租户名称" autocomplete="tenant_name"
|
<input v-model="tenant_name" type="text" placeholder="租户名称" autocomplete="tenant_name"
|
||||||
class="input input-with-icon" />
|
class="input input-with-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group icon-input-group">
|
<div v-if="loginMode === 'password'" class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-user"></i>
|
||||||
</span>
|
</span>
|
||||||
<input v-model="account" type="text" placeholder="用户名" autocomplete="account" class="input input-with-icon" />
|
<input v-model="account" type="text" placeholder="用户名" autocomplete="account" class="input input-with-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group icon-input-group">
|
<div v-if="loginMode === 'password'" class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-lock"></i>
|
<i class="fa-solid fa-lock"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -45,16 +49,29 @@
|
|||||||
<i v-else class="fa-solid fa-eye-slash"></i>
|
<i v-else class="fa-solid fa-eye-slash"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="loginMode === 'sms'" class="form-group icon-input-group">
|
||||||
|
<span class="input-icon">
|
||||||
|
<i class="fa-solid fa-mobile-screen-button"></i>
|
||||||
|
</span>
|
||||||
|
<input v-model="phone" type="text" placeholder="手机号" autocomplete="tel" class="input input-with-icon" />
|
||||||
|
</div>
|
||||||
|
<div v-if="loginMode === 'sms'" class="form-group code-row">
|
||||||
|
<input v-model="smsCode" type="text" placeholder="短信验证码" class="input code-input" />
|
||||||
|
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendLoginCode">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 极验验证码容器 -->
|
<!-- 极验验证码容器 -->
|
||||||
<div style="display: none;" v-if="showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
|
<div style="display: none;" v-if="showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
|
||||||
|
|
||||||
<div class="remember-me-row">
|
<div class="remember-me-row">
|
||||||
<label class="remember-me-label">
|
<label v-if="loginMode === 'password'" class="remember-me-label">
|
||||||
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" @change="handleRememberMeChange" />
|
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" @change="handleRememberMeChange" />
|
||||||
<span>记住我</span>
|
<span>记住我</span>
|
||||||
</label>
|
</label>
|
||||||
|
<span v-else></span>
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
<a class="register-link" @click.prevent="goRegister">注册账号</a>
|
<a class="register-link" @click.prevent="goRegister">注册账号</a>
|
||||||
<span class="divider">|</span>
|
<span class="divider">|</span>
|
||||||
@ -76,10 +93,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from "vue";
|
import { ref, onMounted, nextTick, onUnmounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { login, getOpenVerify, getGeetest4Infos } from "@/api/login";
|
import { login, getOpenVerify, getGeetest4Infos, sendLoginCode, loginBySms } from "@/api/login";
|
||||||
import "@/assets/js/gt4.js";
|
import "@/assets/js/gt4.js";
|
||||||
import { ElMessageBox, ElMessage } from "element-plus";
|
import { ElMessageBox, ElMessage } from "element-plus";
|
||||||
|
|
||||||
@ -88,11 +105,17 @@ const authStore = useAuthStore();
|
|||||||
|
|
||||||
// --- 表单数据 ---
|
// --- 表单数据 ---
|
||||||
const tenant_name = ref("");
|
const tenant_name = ref("");
|
||||||
|
const loginMode = ref<"password" | "sms">("password");
|
||||||
const account = ref("");
|
const account = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
const phone = ref("");
|
||||||
|
const smsCode = ref("");
|
||||||
const passwordVisible = ref(false);
|
const passwordVisible = ref(false);
|
||||||
const rememberMe = ref(false);
|
const rememberMe = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
const errorMsg = ref("");
|
const errorMsg = ref("");
|
||||||
|
|
||||||
// --- 极验相关变量 ---
|
// --- 极验相关变量 ---
|
||||||
@ -127,6 +150,25 @@ const cleanCaptchaInstance = () => {
|
|||||||
|
|
||||||
// --- 执行登录请求 ---
|
// --- 执行登录请求 ---
|
||||||
const performLoginRequest = async () => {
|
const performLoginRequest = async () => {
|
||||||
|
if (loginMode.value === "sms") {
|
||||||
|
const res = await loginBySms({
|
||||||
|
tenant_name: tenant_name.value,
|
||||||
|
phone: phone.value,
|
||||||
|
sms_code: smsCode.value,
|
||||||
|
});
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
authStore.setLoginInfo(res.data);
|
||||||
|
const { useTabsStore } = await import("@/stores");
|
||||||
|
const tabsStore = useTabsStore();
|
||||||
|
tabsStore.resetTabs();
|
||||||
|
router.push({ path: "/home" });
|
||||||
|
ElMessage.success("登录成功!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
errorMsg.value = res.msg || "登录失败";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await login({
|
const res = await login({
|
||||||
tenant_name: tenant_name.value,
|
tenant_name: tenant_name.value,
|
||||||
account: account.value,
|
account: account.value,
|
||||||
@ -163,6 +205,53 @@ const performLoginRequest = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const switchMode = (mode: "password" | "sms") => {
|
||||||
|
loginMode.value = mode;
|
||||||
|
errorMsg.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendLoginCode = async () => {
|
||||||
|
if (codeLoading.value || countdown.value > 0) return;
|
||||||
|
if (!tenant_name.value.trim()) {
|
||||||
|
errorMsg.value = "请输入租户名称";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!phone.value.trim()) {
|
||||||
|
errorMsg.value = "请输入手机号";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
codeLoading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await sendLoginCode({
|
||||||
|
tenant_name: tenant_name.value,
|
||||||
|
phone: phone.value,
|
||||||
|
});
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
ElMessage.success("验证码已发送");
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res?.msg || "发送验证码失败";
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMsg.value = err?.response?.data?.msg || err?.message || "发送验证码失败";
|
||||||
|
} finally {
|
||||||
|
codeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- 初始化极验4.0 ---
|
// --- 初始化极验4.0 ---
|
||||||
const startGeetest4 = async () => {
|
const startGeetest4 = async () => {
|
||||||
showCaptchaContainer.value = true;
|
showCaptchaContainer.value = true;
|
||||||
@ -277,34 +366,50 @@ const handleLogin = async () => {
|
|||||||
errorMsg.value = "请输入租户名称";
|
errorMsg.value = "请输入租户名称";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!account.value.trim()) {
|
if (loginMode.value === "password") {
|
||||||
errorMsg.value = "请输入用户名";
|
if (!account.value.trim()) {
|
||||||
return;
|
errorMsg.value = "请输入用户名";
|
||||||
}
|
return;
|
||||||
if (!password.value.trim()) {
|
}
|
||||||
errorMsg.value = "请输入密码";
|
if (!password.value.trim()) {
|
||||||
return;
|
errorMsg.value = "请输入密码";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!phone.value.trim()) {
|
||||||
|
errorMsg.value = "请输入手机号";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!smsCode.value.trim()) {
|
||||||
|
errorMsg.value = "请输入短信验证码";
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先判断是否开启验证
|
if (loginMode.value === "password") {
|
||||||
const verifyRes = await getOpenVerify();
|
// 先判断是否开启验证
|
||||||
let openVerify = "0";
|
const verifyRes = await getOpenVerify();
|
||||||
|
let openVerify = "0";
|
||||||
|
|
||||||
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
|
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
|
||||||
verifyRes.data.forEach((item: any) => {
|
verifyRes.data.forEach((item: any) => {
|
||||||
if (item.label === "openVerify") {
|
if (item.label === "openVerify") {
|
||||||
openVerify = item.value || "0";
|
openVerify = item.value || "0";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果开启了验证,使用极验4.0
|
// 如果开启了验证,使用极验4.0
|
||||||
if (openVerify === "1") {
|
if (openVerify === "1") {
|
||||||
await startGeetest4();
|
await startGeetest4();
|
||||||
|
} else {
|
||||||
|
// 直接登录
|
||||||
|
await performLoginRequest();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接登录
|
// 直接登录
|
||||||
await performLoginRequest();
|
await performLoginRequest();
|
||||||
@ -348,6 +453,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
});
|
||||||
|
|
||||||
const goRegister = () => router.push("/register");
|
const goRegister = () => router.push("/register");
|
||||||
const goForget = () => router.push("/forget");
|
const goForget = () => router.push("/forget");
|
||||||
</script>
|
</script>
|
||||||
@ -447,6 +556,29 @@ const goForget = () => router.push("/forget");
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mode-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #d6e6fa;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f7fbfe;
|
||||||
|
color: #407ad6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn.active {
|
||||||
|
background: #e8f3ff;
|
||||||
|
border-color: #7cb8ff;
|
||||||
|
color: #1d63c2;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -520,6 +652,32 @@ const goForget = () => router.push("/forget");
|
|||||||
box-shadow: 0 0 0 2px #e3f2ffb1;
|
box-shadow: 0 0 0 2px #e3f2ffb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
width: 120px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1.3px solid #d6e6fa;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
color: #3a78ca;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* 极验验证码容器样式 */
|
/* 极验验证码容器样式 */
|
||||||
.geetest-container {
|
.geetest-container {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|||||||
131
src/views/login/register.vue
Normal file
131
src/views/login/register.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="card">
|
||||||
|
<h2>注册账号</h2>
|
||||||
|
<p class="desc">按租户创建管理员账号</p>
|
||||||
|
|
||||||
|
<input v-model="form.tenant_name" class="input" placeholder="租户名称" />
|
||||||
|
<input v-model="form.account" class="input" placeholder="账号" />
|
||||||
|
<input v-model="form.name" class="input" placeholder="姓名" />
|
||||||
|
<input v-model="form.phone" class="input" placeholder="手机号" />
|
||||||
|
<div class="code-row">
|
||||||
|
<input v-model="form.sms_code" class="input code-input" placeholder="短信验证码" />
|
||||||
|
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendCode">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input v-model="form.email" class="input" placeholder="邮箱(可选)" />
|
||||||
|
<input v-model="form.password" class="input" type="password" placeholder="密码" />
|
||||||
|
<input v-model="form.confirm_password" class="input" type="password" placeholder="确认密码" />
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
|
||||||
|
<button class="btn" :disabled="loading" @click="handleSubmit">
|
||||||
|
{{ loading ? "提交中..." : "注 册" }}
|
||||||
|
</button>
|
||||||
|
<a class="back" @click.prevent="router.push('/login')">返回登录</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onUnmounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { register, sendRegisterCode } from "@/api/login";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
const errorMsg = ref("");
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
tenant_name: "",
|
||||||
|
account: "",
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
sms_code: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirm_password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (codeLoading.value || countdown.value > 0) return;
|
||||||
|
if (!form.tenant_name || !form.account || !form.phone) {
|
||||||
|
errorMsg.value = "请先填写租户名称、账号和手机号";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
codeLoading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await sendRegisterCode({
|
||||||
|
tenant_name: form.tenant_name,
|
||||||
|
account: form.account,
|
||||||
|
phone: form.phone,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("验证码已发送");
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "验证码发送失败";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMsg.value = "验证码发送失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
codeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
errorMsg.value = "";
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await register(form);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("注册成功,请登录");
|
||||||
|
router.push("/login");
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "注册失败";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = "注册失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f4f8ff; }
|
||||||
|
.card { width: 420px; background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 8px 24px rgba(0,0,0,.08); }
|
||||||
|
.desc { color: #6b7b93; margin-bottom: 12px; }
|
||||||
|
.input { width: 100%; margin-bottom: 10px; padding: 10px; border: 1px solid #dbe3ee; border-radius: 6px; box-sizing: border-box; }
|
||||||
|
.code-row { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.code-input { margin-bottom: 0; flex: 1; }
|
||||||
|
.code-btn { width: 120px; border: 1px solid #dbe3ee; border-radius: 6px; background: #fff; cursor: pointer; }
|
||||||
|
.code-btn:disabled { opacity: .6; cursor: not-allowed; }
|
||||||
|
.btn { width: 100%; margin-top: 8px; padding: 10px; border: none; border-radius: 6px; background: #3f8cff; color: #fff; cursor: pointer; }
|
||||||
|
.btn:disabled { opacity: .65; cursor: not-allowed; }
|
||||||
|
.error { color: #e34d4d; font-size: 13px; margin: 6px 0; }
|
||||||
|
.back { display: inline-block; margin-top: 10px; color: #3f8cff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
|
||||||
149
src/views/system/smssettings/components/edit.vue
Normal file
149
src/views/system/smssettings/components/edit.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sms-edit">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="120px"
|
||||||
|
class="sms-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="短信网关地址" prop="backendUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.backendUrl"
|
||||||
|
placeholder="例如:https://yzsms.yunzer.cn"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="短信API KEY" prop="apiKey">
|
||||||
|
<el-input v-model="form.apiKey" placeholder="请输入 API_KEY" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="测试收件手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="testPhone"
|
||||||
|
placeholder="国际格式号码,例如 +8613712345678"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleTest" :loading="testLoading">发送测试短信</el-button>
|
||||||
|
<el-button @click="handleReset" :disabled="saveLoading || testLoading">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
import { editSmsInfo, sendTestSms } from "../../../../api/sms";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: {
|
||||||
|
backendUrl?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "saved"): void;
|
||||||
|
(e: "tested"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
const form = reactive({
|
||||||
|
backendUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
backendUrl: [{ required: true, message: "请输入短信网关地址", trigger: "blur" }],
|
||||||
|
apiKey: [{ required: true, message: "请输入API KEY", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const testPhone = ref("");
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
const testLoading = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.config,
|
||||||
|
(v) => {
|
||||||
|
form.backendUrl = v?.backendUrl ?? "";
|
||||||
|
form.apiKey = v?.apiKey ?? "";
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
saveLoading.value = true;
|
||||||
|
await formRef.value.validate();
|
||||||
|
const res = await editSmsInfo(form);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.msg || "短信配置保存成功");
|
||||||
|
emit("saved");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "保存失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || "保存失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
const phone = (testPhone.value || "").trim();
|
||||||
|
if (!phone) {
|
||||||
|
ElMessage.warning("请先输入测试收件手机号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Android 网关要求国际格式:以 + 开头,后面为数字
|
||||||
|
if (!/^\+\d{6,15}$/.test(phone)) {
|
||||||
|
ElMessage.warning("请使用国际格式号码(以 + 开头,后面为数字)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
testLoading.value = true;
|
||||||
|
await formRef.value.validate();
|
||||||
|
const res = await sendTestSms({ ...form, phone });
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.msg || "短信测试任务入队成功");
|
||||||
|
emit("tested");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "短信测试失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || "短信测试失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
testLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.backendUrl = "";
|
||||||
|
form.apiKey = "";
|
||||||
|
testPhone.value = "";
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.sms-edit {
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-form {
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
60
src/views/system/smssettings/index.vue
Normal file
60
src/views/system/smssettings/index.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container-box">
|
||||||
|
<div class="header-bar">
|
||||||
|
<h2>短信配置</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="content-box">
|
||||||
|
<Edit :config="smsForm" @saved="loadSmsConfig" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, onMounted } from "vue";
|
||||||
|
import Edit from "./components/edit.vue";
|
||||||
|
import { getSmsInfo } from "../../../api/sms";
|
||||||
|
|
||||||
|
const smsForm = reactive({
|
||||||
|
backendUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadSmsConfig = async () => {
|
||||||
|
const res = await getSmsInfo();
|
||||||
|
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
|
||||||
|
const item = res.data[0];
|
||||||
|
smsForm.backendUrl = item.backend_url || item.backendUrl || "";
|
||||||
|
smsForm.apiKey = item.api_key || item.apiKey || "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSmsConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
189
src/views/system/smssettings/tasklist.vue
Normal file
189
src/views/system/smssettings/tasklist.vue
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-box">
|
||||||
|
<div class="header-bar">
|
||||||
|
<h2>短信任务列表</h2>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="fetchTasks" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-form :inline="true" :model="searchForm">
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.phone"
|
||||||
|
placeholder="请输入手机号关键词"
|
||||||
|
style="width: 220px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="全部" style="width: 160px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="待发送" :value="0" />
|
||||||
|
<el-option label="处理中" :value="1" />
|
||||||
|
<el-option label="失败" :value="2" />
|
||||||
|
<el-option label="已上报" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="pagedList" style="width: 100%" border>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="phone" label="手机号" min-width="150" />
|
||||||
|
<el-table-column
|
||||||
|
prop="content"
|
||||||
|
label="短信内容"
|
||||||
|
min-width="260"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column prop="code" label="验证码" width="120" />
|
||||||
|
<el-table-column prop="status" label="状态" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="创建时间" width="180" />
|
||||||
|
<el-table-column prop="update_time" label="更新时间" width="180" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@size-change="applyPagination"
|
||||||
|
@current-change="applyPagination"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, onMounted } from "vue";
|
||||||
|
import { Refresh } from "@element-plus/icons-vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getSmsTaskList } from "../../../api/sms";
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const searchForm = reactive({
|
||||||
|
phone: "",
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = ref<any[]>([]);
|
||||||
|
const pagedList = ref<any[]>([]);
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
const getStatusTagType = (status: any) => {
|
||||||
|
const map: Record<string | number, string> = {
|
||||||
|
0: "info",
|
||||||
|
1: "warning",
|
||||||
|
2: "danger",
|
||||||
|
3: "success",
|
||||||
|
};
|
||||||
|
return map[status] || "info";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: any) => {
|
||||||
|
const map: Record<string | number, string> = {
|
||||||
|
0: "待发送",
|
||||||
|
1: "发送中",
|
||||||
|
2: "发送失败",
|
||||||
|
3: "发送成功",
|
||||||
|
};
|
||||||
|
return map[status] || String(status ?? "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPagination = () => {
|
||||||
|
total.value = tasks.value.length;
|
||||||
|
const start = (pagination.current - 1) * pagination.size;
|
||||||
|
const end = start + pagination.size;
|
||||||
|
pagedList.value = tasks.value.slice(start, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTasks = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getSmsTaskList({
|
||||||
|
status: searchForm.status,
|
||||||
|
phone: searchForm.phone,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
tasks.value = res.list || [];
|
||||||
|
pagination.current = 1;
|
||||||
|
applyPagination();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "获取短信任务列表失败");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || "获取短信任务列表失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1;
|
||||||
|
fetchTasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.phone = "";
|
||||||
|
searchForm.status = "";
|
||||||
|
pagination.current = 1;
|
||||||
|
fetchTasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTasks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user