更新代码

This commit is contained in:
2026-09-15 10:44:31 +08:00
parent 9875832019
commit 943c3708b0
125 changed files with 2009 additions and 1728 deletions
+1 -6
View File
@@ -117,6 +117,7 @@ import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand, Loa
import { ElMessage } from 'element-plus';
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
import MessageDetailDialog from "@/views/basicSettings/sitereminder/components/detail.vue";
import { formatTime } from "@/utils/datetime";
const router = useRouter();
const route = useRoute();
@@ -178,12 +179,6 @@ const loadingMessages = ref(false);
const detailVisible = ref(false);
const currentReminder = ref<any>({});
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
const fetchUnreadCount = async () => {
if (!authStore.token) return;
try {
+55
View File
@@ -0,0 +1,55 @@
/**
* 统一日期时间格式化工具(platform 端)
*
* 后端(Go)返回的时间字段为 RFC3339 / ISO 8601 字符串,
* 例如 "2026-03-31T23:34:07+08:00",直接渲染不友好。
* 页面上显示时间统一使用本工具,输出:
* formatDateTime / formatTime -> 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss)
* formatDate / formatDateOnly -> 2026-03-31(YYYY-MM-DD)
*/
const pad = (n: number) => String(n).padStart(2, "0");
function toDate(value: any): Date | null {
if (value === null || value === undefined || value === "") return null;
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
// 兼容秒级 / 毫秒级时间戳
if (typeof value === "number") {
const d = new Date(value < 1e12 ? value * 1000 : value);
return isNaN(d.getTime()) ? null : d;
}
// "2026-03-31 23:34:07" 形式在部分浏览器解析异常,统一先替换为 ISO 形式
const str = String(value).trim().replace(" ", "T");
const d = new Date(str);
return isNaN(d.getTime()) ? null : d;
}
/** 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss);非法值返回 "-" */
export function formatDateTime(value: any): string {
const d = toDate(value);
if (!d) return "-";
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
);
}
/** 与 formatDateTime 相同(兼容旧调用名) */
export function formatTime(value: any): string {
return formatDateTime(value);
}
/** 2026-03-31(YYYY-MM-DD);非法值返回 "-";纯日期串不做时区换算 */
export function formatDate(value: any): string {
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
return value.trim();
}
const d = toDate(value);
if (!d) return "-";
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** 与 formatDate 相同(兼容旧调用名) */
export function formatDateOnly(value: any): string {
return formatDate(value);
}
@@ -48,10 +48,10 @@
{{ user.login_count || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ user.create_time || "未知" }}
{{ user.create_time ? formatDateTime(user.create_time) : "未知" }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ user.update_time || "未知" }}
{{ user.update_time ? formatDateTime(user.update_time) : "未知" }}
</el-descriptions-item>
</el-descriptions>
</div>
@@ -66,6 +66,7 @@
import { ref, watch } from "vue";
import { UserFilled } from "@element-plus/icons-vue";
import { getAllRoles } from "@/api/role";
import { formatDateTime } from "@/utils/datetime";
interface User {
id: number;
+4 -1
View File
@@ -76,7 +76,9 @@
<el-tag v-else :type="sslTagType(scope.row.ssl_status)">{{ sslText(scope.row.ssl_status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column prop="create_time" label="申请时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<!-- 自有域名无人工审核:解析与证书就绪后自动生效,这里只提供重新检测与禁用 -->
@@ -134,6 +136,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/datetime'
import {
getTenantDomainList,
auditTenantDomain,
+4 -1
View File
@@ -49,7 +49,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="scope">
<el-button size="small" text @click="handleEdit(scope.row)">
@@ -107,6 +109,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Refresh, Switch } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/datetime'
import {
getDomainPoolList,
createDomainPool,
@@ -33,10 +33,10 @@
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ roleDetail.create_time }}
{{ formatDateTime(roleDetail.create_time) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ roleDetail.update_time }}
{{ formatDateTime(roleDetail.update_time) }}
</el-descriptions-item>
</el-descriptions>
@@ -51,6 +51,7 @@ import { ref, watch, computed } from "vue";
import { ElMessage } from "element-plus";
import { getRoleById } from "@/api/role";
import { getAllMenus } from "@/api/menu";
import { formatDateTime } from "@/utils/datetime";
interface Props {
modelValue: boolean;
+117 -11
View File
@@ -23,7 +23,16 @@
<!-- 角色列表 -->
<div v-else>
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
<div class="scope-filter">
<el-radio-group v-model="scopeFilter" size="small">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="platform">平台角色</el-radio-button>
<el-radio-button label="global">全局角色</el-radio-button>
<el-radio-button label="tenant">租户角色</el-radio-button>
</el-radio-group>
</div>
<el-table :data="displayRoles" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column
prop="name"
@@ -31,26 +40,41 @@
min-width="150"
align="center"
/>
<el-table-column prop="cid" label="角色分类" width="120" align="center">
<el-table-column prop="cid" label="角色分类" width="110" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.cid) === 2 ? 'warning' : 'primary'" size="small">
{{ Number(row.cid) === 2 ? "租户角色" : "平台角色" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<el-table-column label="所属范围" min-width="150" align="center">
<template #default="{ row }">
{{ getScopeLabel(row) }}
</template>
</el-table-column>
<el-table-column label="类型" width="90" align="center">
<template #default="{ row }">
<el-tag
:type="Number(row.is_custom) === 1 ? 'warning' : 'info'"
size="small"
effect="plain"
>
{{ Number(row.is_custom) === 1 ? "自建" : "预置" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="rights"
label="权限"
min-width="150"
align="center"
/>
<el-table-column label="权限" min-width="110" align="center">
<template #default="{ row }">
{{ formatRights(row.rights) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button
@@ -101,10 +125,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
import { getAllRoles, getRoleById, deleteRole } from "@/api/role";
import { getTenantList } from "@/api/tenant";
import RoleEditDialog from "./components/edit.vue";
import RoleDetailDialog from "./components/detail.vue";
import { useAuthStore } from "@/stores/auth";
@@ -116,6 +141,82 @@ const roles = ref<any[]>([]);
const loading = ref(false);
const error = ref("");
// 范围筛选:all 全部 / platform 平台角色 / global 全局角色 / tenant 租户角色
const scopeFilter = ref("all");
// 租户ID -> 租户名称 映射(用于展示角色所属租户)
const tenantNameMap = ref<Record<number, string>>({});
// 角色展示排序:平台角色 → 全局角色 → 租户角色,同级按 id 升序
const sortRoles = (list: any[]) => {
// cid 非 2(含历史数据 cid 为空)视为平台角色,排在全局/租户角色之前
const rank = (r: any) =>
Number(r.cid) === 2 ? (Number(r.tenant_id || 0) === 0 ? 1 : 2) : 0;
return [...list].sort((a, b) => {
const ra = rank(a);
const rb = rank(b);
if (ra !== rb) return ra - rb;
return Number(a.id) - Number(b.id);
});
};
const displayRoles = computed(() => {
if (scopeFilter.value === "platform")
return roles.value.filter((r) => Number(r.cid) !== 2);
if (scopeFilter.value === "global")
return roles.value.filter(
(r) => Number(r.cid) === 2 && Number(r.tenant_id || 0) === 0
);
if (scopeFilter.value === "tenant")
return roles.value.filter(
(r) => Number(r.cid) === 2 && Number(r.tenant_id || 0) > 0
);
return roles.value;
});
// 所属范围:平台 / 全局(所有租户)/ 具体租户名
const getScopeLabel = (row: any) => {
if (Number(row.cid) !== 2) return "平台";
const tid = Number(row.tenant_id || 0);
if (tid === 0) return "全局(所有租户)";
return tenantNameMap.value[tid] || `租户ID: ${tid}`;
};
// 权限展示:空 = 全权限;有值展示权限项数量
const formatRights = (rights: any) => {
if (rights === null || rights === undefined) return "全权限";
let ids: any[] = [];
if (Array.isArray(rights)) {
ids = rights;
} else if (typeof rights === "string") {
const s = rights.trim();
if (!s) return "全权限";
try {
const arr = JSON.parse(s);
ids = Array.isArray(arr) ? arr : [s];
} catch {
ids = s.split(",").filter((x) => x.trim() !== "");
}
}
return ids.length === 0 ? "全权限" : `${ids.length} 项`;
};
// 租户名映射(角色所属范围展示用;缺失时退化为显示租户ID)
const loadTenantNames = async () => {
try {
const res = await getTenantList({ page: 1, pageSize: 1000 });
if (res.code === 200) {
const list = res.data?.list || res.data || [];
const map: Record<number, string> = {};
list.forEach((t: any) => {
map[Number(t.id)] = t.tenant_name;
});
tenantNameMap.value = map;
}
} catch {
// ignore
}
};
const editDialogVisible = ref(false);
const detailDialogVisible = ref(false);
const currentRole = ref<any>(null);
@@ -138,7 +239,7 @@ const fetchRoles = async () => {
try {
const res = await getAllRoles();
if (res.code === 200) {
roles.value = res.data || [];
roles.value = sortRoles(res.data || []);
// console.log('角色列表:', roles.value);
} else {
error.value = res.msg || "获取角色列表失败";
@@ -213,10 +314,15 @@ function handleEditSuccess() {
onMounted(() => {
fetchRoles();
loadTenantNames();
});
</script>
<style scoped>
.scope-filter {
margin-bottom: 12px;
}
.error-state {
display: flex;
flex-direction: column;
@@ -32,6 +32,7 @@
import { computed, ref, watch } from "vue";
import { User, Calendar } from "@element-plus/icons-vue";
import { readSiteReminder } from "@/api/sitereminder";
import { formatTime } from "@/utils/datetime";
const props = defineProps({
modelValue: Boolean,
@@ -62,12 +63,6 @@ const getSenderName = (item: any) => {
return item.sender_type;
};
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
watch(() => props.modelValue, async (val) => {
if (val && props.reminder && !props.viewOnly && props.reminder.is_read === 0) {
loading.value = true;
@@ -26,14 +26,48 @@
</el-form-item>
<el-form-item label="接收目标" prop="target_type">
<el-radio-group v-model="form.target_type" @change="handleTargetTypeChange">
<el-radio-group
v-model="form.target_type"
class="target-radio-group"
@change="handleTargetTypeChange"
>
<el-radio-button label="all">全平台</el-radio-button>
<el-radio-button label="platform">平台端</el-radio-button>
<el-radio-button label="tenant_all">管理端 (所有租户)</el-radio-button>
<el-radio-button label="role">平台角色</el-radio-button>
<el-radio-button label="tenant">特定租户</el-radio-button>
<el-radio-button label="tenant_all">租户端</el-radio-button>
<el-radio-button label="tenant_role">租户角色</el-radio-button>
<el-radio-button label="tenant">租户</el-radio-button>
<el-radio-button label="tenant_user">租户用户</el-radio-button>
<el-radio-button v-if="isLegacyRole" label="role">平台角色</el-radio-button>
</el-radio-group>
</el-form-item>
<!-- 租户角色(可多选) -->
<el-form-item
v-if="form.target_type === 'tenant_role'"
label="选择角色"
prop="target_ids"
:rules="[{ required: true, type: 'array', min: 1, message: '请选择接收的租户角色', trigger: 'change' }]"
>
<el-select
v-model="form.target_ids"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="请选择租户角色(可多选)"
v-loading="loadingRoles"
style="width: 100%;"
>
<el-option
v-for="item in tenantRolesList"
:key="item.id"
:label="getRoleLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 旧数据:平台角色 -->
<el-form-item
v-if="form.target_type === 'role'"
label="选择角色"
@@ -47,7 +81,7 @@
style="width: 100%;"
>
<el-option
v-for="item in rolesList"
v-for="item in platformRolesList"
:key="item.id"
:label="item.name"
:value="item.id"
@@ -55,28 +89,82 @@
</el-select>
</el-form-item>
<!-- 租户(可多选,默认发送给所选租户所有人员) -->
<el-form-item
v-if="form.target_type === 'tenant'"
label="选择租户"
prop="target_tenant_id"
:rules="[{ required: true, message: '请选择接收租户', trigger: 'change' }]"
prop="target_ids"
:rules="[{ required: true, type: 'array', min: 1, message: '请选择接收租户', trigger: 'change' }]"
>
<el-select
v-model="form.target_tenant_id"
placeholder="请输入租户名称搜索"
v-model="form.target_ids"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="请输入租户名称搜索(可多选,发送给租户全部人员)"
v-loading="loadingTenants"
style="width: 100%;"
>
<el-option
v-for="item in tenantsList"
:key="item.id"
:label="`${item.name} (${item.tenant_code})`"
:label="`${item.tenant_name} (${item.tenant_code})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 租户用户(先选租户,再选该租户下用户,可多选) -->
<template v-if="form.target_type === 'tenant_user'">
<el-form-item
label="选择租户"
prop="user_tenant_id"
:rules="[{ required: true, message: '请选择租户', trigger: 'change' }]"
>
<el-select
v-model="form.user_tenant_id"
placeholder="请输入租户名称搜索"
filterable
v-loading="loadingTenants"
style="width: 100%;"
@change="handleUserTenantChange"
>
<el-option
v-for="item in tenantsList"
:key="item.id"
:label="`${item.tenant_name} (${item.tenant_code})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
label="选择用户"
prop="target_ids"
:rules="[{ required: true, type: 'array', min: 1, message: '请选择接收用户', trigger: 'change' }]"
>
<el-select
v-model="form.target_ids"
multiple
filterable
collapse-tags
collapse-tags-tooltip
:placeholder="form.user_tenant_id ? '请选择租户用户(可多选)' : '请先选择租户'"
:disabled="!form.user_tenant_id"
v-loading="loadingTenantUsers"
style="width: 100%;"
>
<el-option
v-for="item in tenantUsersList"
:key="item.id"
:label="getTenantUserLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</template>
<el-form-item label="消息内容" prop="content">
<el-input
v-model="form.content"
@@ -109,6 +197,7 @@ import { Promotion } from "@element-plus/icons-vue";
import { sendSiteReminder, updateSentSiteReminder } from "@/api/sitereminder";
import { getAllRoles } from "@/api/role";
import { getTenantList } from "@/api/tenant";
import { getTenantUserList } from "@/api/tenantUser";
const props = defineProps({
modelValue: Boolean,
@@ -134,38 +223,58 @@ const form = ref({
title: "",
content: "",
target_type: "platform",
target_ids: [] as number[],
target_role_id: null as number | null,
target_tenant_id: null as number | null
user_tenant_id: null as number | null
});
// 需要多选目标ID的接收目标类型
const MULTI_TARGET_TYPES = ["tenant_role", "tenant", "tenant_user"];
// 旧数据兼容:平台角色
const isLegacyRole = computed(() => form.value.target_type === "role");
const rules: FormRules = {
title: [{ required: true, message: "请输入消息标题", trigger: "blur" }],
content: [{ required: true, message: "请输入消息内容", trigger: "blur" }],
target_type: [{ required: true, message: "请选择接收目标", trigger: "change" }]
};
const rolesList = ref<any[]>([]);
const loadingRoles = ref(false);
const platformRolesList = ref<any[]>([]);
const tenantRolesList = ref<any[]>([]);
const tenantsList = ref<any[]>([]);
const tenantUsersList = ref<any[]>([]);
const loadingRoles = ref(false);
const loadingTenants = ref(false);
const loadingTenantUsers = ref(false);
const handleTargetTypeChange = (val: any) => {
form.value.target_role_id = null;
form.value.target_tenant_id = null;
if (val === "role") {
loadRoles();
} else if (val === "tenant") {
loadTenants();
}
const getRoleLabel = (item: any) => {
const tenantName = tenantsList.value.find(
(t) => Number(t.id) === Number(item.tenant_id)
)?.tenant_name;
if (item.tenant_id && tenantName) return `${item.name}(${tenantName})`;
if (item.tenant_id) return `${item.name}(租户ID: ${item.tenant_id})`;
return `${item.name}(通用)`;
};
const getTenantUserLabel = (item: any) => {
const name = item.name || item.account || `用户 ${item.id}`;
const account = item.account && item.name ? `(${item.account})` : "";
return `${name}${account}`;
};
// 加载角色(平台角色 cid=1 / 租户角色 cid=2)
const loadRoles = async () => {
if (rolesList.value.length > 0) return;
if (platformRolesList.value.length > 0 || tenantRolesList.value.length > 0) return;
loadingRoles.value = true;
try {
const res = await getAllRoles();
if (res.code === 200) {
rolesList.value = (res.data || []).filter((r: any) => Number(r.cid) === 1);
const rows: any[] = res.data || [];
platformRolesList.value = rows.filter((r) => Number(r.cid) === 1);
tenantRolesList.value = rows.filter(
(r) => Number(r.cid) === 2 && Number(r.status) === 1
);
}
} catch (err) {
console.error(err);
@@ -174,6 +283,7 @@ const loadRoles = async () => {
}
};
// 加载租户列表
const loadTenants = async () => {
if (tenantsList.value.length > 0) return;
loadingTenants.value = true;
@@ -189,6 +299,81 @@ const loadTenants = async () => {
}
};
// 加载指定租户下的用户
const loadTenantUsers = async (tid: number) => {
if (!tid) {
tenantUsersList.value = [];
return;
}
loadingTenantUsers.value = true;
try {
const res = await getTenantUserList({ tid });
if (res.code === 200) {
tenantUsersList.value = res.data?.list || res.data || [];
}
} catch (err) {
console.error(err);
} finally {
loadingTenantUsers.value = false;
}
};
// 根据用户ID反查所属租户(编辑"租户用户"历史消息时回显租户)
const resolveUserTenant = async (ids: number[]): Promise<number | null> => {
if (!ids.length) return null;
try {
const res = await getTenantUserList({ uid: ids[0] });
if (res.code === 200) {
const rows = res.data?.list || res.data || [];
return rows.length ? Number(rows[0].tid) : null;
}
} catch (err) {
console.error(err);
}
return null;
};
// 解析历史数据的多选目标ID
const parseTargetIds = (raw: any): number[] => {
let ids: any[] = [];
if (Array.isArray(raw)) {
ids = raw;
} else if (typeof raw === "string" && raw.trim()) {
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) ids = arr;
} catch {
// ignore
}
}
return ids.map((n) => Number(n)).filter((n) => !Number.isNaN(n) && n > 0);
};
// 接收目标切换:清空已选目标并预加载选项
const handleTargetTypeChange = (val: any) => {
form.value.target_ids = [];
form.value.target_role_id = null;
form.value.user_tenant_id = null;
tenantUsersList.value = [];
if (val === "tenant_role") {
loadRoles();
loadTenants(); // 用于角色标签中展示所属租户名称
} else if (val === "role") {
loadRoles();
} else if (val === "tenant") {
loadTenants();
} else if (val === "tenant_user") {
loadTenants();
}
};
// "租户用户"模式下切换租户
const handleUserTenantChange = (tid: number) => {
form.value.target_ids = [];
loadTenantUsers(tid);
};
const handleSend = async () => {
if (!formRef.value) return;
try {
@@ -197,16 +382,28 @@ const handleSend = async () => {
return;
}
const needIds = MULTI_TARGET_TYPES.includes(form.value.target_type);
if (needIds && form.value.target_ids.length === 0) {
ElMessage.warning("请选择接收目标");
return;
}
const payload = {
title: form.value.title,
content: form.value.content,
target_type: form.value.target_type,
target_ids: needIds ? form.value.target_ids : [],
target_role_id:
form.value.target_type === "role" ? form.value.target_role_id || 0 : 0,
target_tenant_id: 0
};
submitting.value = true;
try {
if (isEdit.value) {
const res = await updateSentSiteReminder({
batch_id: props.reminder.batch_id,
title: form.value.title,
content: form.value.content,
target_type: form.value.target_type,
target_role_id: form.value.target_role_id || 0,
target_tenant_id: form.value.target_tenant_id || 0
...payload
});
if (res.code === 200) {
ElMessage.success("修改成功");
@@ -216,13 +413,6 @@ const handleSend = async () => {
ElMessage.error(res.msg || "修改失败");
}
} else {
const payload = {
title: form.value.title,
content: form.value.content,
target_type: form.value.target_type,
target_role_id: form.value.target_role_id || 0,
target_tenant_id: form.value.target_tenant_id || 0
};
const res = await sendSiteReminder(payload);
if (res.code === 200) {
ElMessage.success("站内信发布成功");
@@ -239,34 +429,56 @@ const handleSend = async () => {
}
};
watch(() => props.modelValue, (val) => {
if (val) {
watch(
() => props.modelValue,
async (val) => {
if (!val) return;
if (props.reminder) {
const targetType = props.reminder.target_type || "platform";
const ids = parseTargetIds(props.reminder.target_ids);
// 兼容旧版"特定租户"单值
if (targetType === "tenant" && ids.length === 0 && props.reminder.target_tenant_id) {
ids.push(Number(props.reminder.target_tenant_id));
}
form.value = {
title: props.reminder.title,
content: props.reminder.content,
target_type: props.reminder.target_type || "platform",
target_type: targetType,
target_ids: MULTI_TARGET_TYPES.includes(targetType) ? ids : [],
target_role_id: props.reminder.target_role_id || null,
target_tenant_id: props.reminder.target_tenant_id || null
user_tenant_id: null
};
if (form.value.target_type === "role") {
loadRoles();
} else if (form.value.target_type === "tenant") {
loadTenants();
if (targetType === "tenant_role") {
await loadRoles();
await loadTenants();
} else if (targetType === "role") {
await loadRoles();
} else if (targetType === "tenant") {
await loadTenants();
} else if (targetType === "tenant_user") {
await loadTenants();
const tid = await resolveUserTenant(ids);
if (tid) {
form.value.user_tenant_id = tid;
await loadTenantUsers(tid);
}
}
} else {
form.value = {
title: "",
content: "",
target_type: "platform",
target_ids: [],
target_role_id: null,
target_tenant_id: null
user_tenant_id: null
};
rolesList.value = [];
tenantsList.value = [];
tenantUsersList.value = [];
}
}
});
);
</script>
<style scoped>
@@ -275,4 +487,10 @@ watch(() => props.modelValue, (val) => {
justify-content: flex-end;
gap: 12px;
}
.target-radio-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>
@@ -105,6 +105,7 @@ import {
} from "@/api/sitereminder";
import MessageComposeDialog from "./components/edit.vue";
import MessageDetailDialog from "./components/detail.vue";
import { formatTime } from "@/utils/datetime";
const loading = ref(false);
const reminderList = ref<any[]>([]);
@@ -124,41 +125,65 @@ const currentReminder = ref<any>({});
// 接收目标标签类型
const getTargetTagType = (type: string) => {
switch (type) {
case "all":
return "danger";
case "platform":
return "primary";
case "tenant_all":
return "success";
case "role":
case "tenant_role":
return "warning";
case "tenant":
return "danger";
return "warning";
case "tenant_user":
return "info";
case "role":
return "warning";
default:
return "info";
}
};
// 解析接收目标多选ID数量(兼容数组 / JSON 字符串)
const getTargetIdsCount = (row: any) => {
const raw = row?.target_ids;
if (!raw) return 0;
if (Array.isArray(raw)) return raw.length;
if (typeof raw === "string") {
try {
const arr = JSON.parse(raw);
return Array.isArray(arr) ? arr.length : 0;
} catch {
return 0;
}
}
return 0;
};
// 接收目标显示文字
const getTargetLabel = (row: any) => {
const count = getTargetIdsCount(row);
switch (row.target_type) {
case "all":
return "全平台(所有人)";
case "platform":
return "平台端";
return "平台端(所有人)";
case "tenant_all":
return "管理端 (所有租户)";
return "租户端(所有租户)";
case "tenant_role":
return count > 0 ? `租户角色(${count}个)` : "租户角色";
case "tenant":
if (count > 0) return `租户(${count}个)`;
return row.target_tenant_id ? `租户(ID: ${row.target_tenant_id})` : "租户";
case "tenant_user":
return count > 0 ? `租户用户(${count}人)` : "租户用户";
case "role":
return `平台角色 (ID: ${row.target_role_id})`;
case "tenant":
return `特定租户 (ID: ${row.target_tenant_id})`;
default:
return row.target_type || "未知";
}
};
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
// 获取已发送列表
const fetchList = async () => {
loading.value = true;
@@ -36,7 +36,7 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ detailData.create_time || '-' }}
{{ detailData.create_time ? formatDateTime(detailData.create_time) : '-' }}
</el-descriptions-item>
</el-descriptions>
@@ -68,6 +68,7 @@
import { ref } from "vue";
import { getTenantDetail } from "@/api/tenant";
import TenantUsersTab from "./TenantUsersTab.vue";
import { formatDateTime } from "@/utils/datetime";
const visible = ref(false);
const loading = ref(false);
@@ -104,7 +104,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column prop="create_time" label="申请时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<el-button
@@ -262,6 +264,7 @@ import {
getCustomDomainGuide,
} from "@/api/domain";
import { getTenantList } from "@/api/tenant";
import { formatDateTime } from "@/utils/datetime";
// 域名类型:与后端 models.DomainTypeCustom 对应
const DOMAIN_TYPE_CUSTOM = 2;
@@ -48,10 +48,10 @@
{{ user.login_count || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ user.create_time || "未知" }}
{{ user.create_time ? formatDateTime(user.create_time) : "未知" }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ user.update_time || "未知" }}
{{ user.update_time ? formatDateTime(user.update_time) : "未知" }}
</el-descriptions-item>
</el-descriptions>
</div>
@@ -66,6 +66,7 @@
import { ref, watch } from "vue";
import { UserFilled } from "@element-plus/icons-vue";
import { getAllRoles } from "@/api/role";
import { formatDateTime } from "@/utils/datetime";
interface User {
id: number;
@@ -100,6 +100,8 @@ import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import { getModuleCategory,editModuleCategory } from "@/api/moduleCenter";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
interface Category {
id: number;
@@ -160,16 +162,7 @@ const getStatusType = (status: number) => {
return map[status] || "info";
};
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handleCategoryAdd = () => {
isEdit.value = false;
+3 -10
View File
@@ -146,6 +146,8 @@ import type { FormInstance, FormRules } from "element-plus";
import { getModules, getModuleCategory,editModules } from "@/api/moduleCenter";
import CreateModules from "../components/createModules.vue";
import { uploadFile } from '@/api/file.js';
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
interface Module {
id: number;
@@ -266,16 +268,7 @@ function handlePictureCardPreview(file: any) {
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handleCreate = () => {
createDialogVisible.value = true;
@@ -172,6 +172,8 @@
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, View, Delete, UploadFilled, Document } from "@element-plus/icons-vue";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
import type { FormInstance, FormRules, UploadFile } from "element-plus";
import { getModules, getModuleCategory } from "@/api/moduleCenter";
@@ -245,16 +247,7 @@ const getStatusType = (status: number) => {
return map[status] || "info";
};
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handlePublish = () => {
publishDialogVisible.value = true;
@@ -280,6 +280,8 @@
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getFileUrl as getFileUrlUtil } from "@/utils/url";
// 日期显示统一为:YYYY-MM-DD
import { formatDate } from "@/utils/datetime";
import {
Folder,
Grid,
@@ -683,14 +685,7 @@ const formatFileSize = (bytes: number): string => {
};
// 格式化日期
const formatDate = (dateStr: string): string => {
const date = new Date(dateStr);
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
// 文件操作
const handleFileClick = (file: FileItem) => {
@@ -28,7 +28,7 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="登录时间" label-width="150px">
{{ log.create_time }}
{{ formatDateTime(log.create_time) }}
</el-descriptions-item>
<el-descriptions-item label="IP地址" label-width="150px">
{{ log.ip || "-" }}
@@ -50,6 +50,7 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { getLoginLogDetail } from "@/api/loginLog";
import { formatDateTime } from "@/utils/datetime";
interface LoginLog {
id: number;
+4 -6
View File
@@ -128,12 +128,9 @@
show-overflow-tooltip
/>
<el-table-column prop="ip" label="IP地址" width="130" align="center" />
<el-table-column
prop="create_time"
label="登录时间"
width="180"
align="center"
/>
<el-table-column prop="create_time" label="登录时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button
@@ -176,6 +173,7 @@ import { ElMessage } from "element-plus";
import { Refresh, Search } from "@element-plus/icons-vue";
import { getLoginLogs } from "@/api/loginLog";
import DetailDialog from "./components/detail.vue";
import { formatDateTime } from "@/utils/datetime";
interface LoginLog {
id: number;
@@ -34,7 +34,7 @@
{{ log.ip }}
</el-descriptions-item>
<el-descriptions-item label="操作时间" label-width="150px" :span="2">
{{ log.create_time }}
{{ formatDateTime(log.create_time) }}
</el-descriptions-item>
<el-descriptions-item label="用户代理" label-width="150px" :span="2">
<div class="user-agent-text">{{ log.user_agent || "无" }}</div>
@@ -74,6 +74,7 @@
import { ref, watch } from "vue";
import { Document } from "@element-plus/icons-vue";
import { getOperationLogDetail } from "@/api/operationLog";
import { formatDateTime } from "@/utils/datetime";
interface OperationLog {
id: number;
@@ -117,12 +117,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="create_time"
label="操作时间"
width="180"
align="center"
/>
<el-table-column prop="create_time" label="操作时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button
@@ -170,6 +167,7 @@ import {
getOperationStatistics,
} from "@/api/operationLog";
import DetailDialog from "./components/detail.vue";
import { formatDateTime } from "@/utils/datetime";
interface OperationLog {
id: number;
@@ -58,7 +58,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
</el-table>
<div class="pagination" v-if="total > 0">
@@ -81,6 +83,7 @@ import { reactive, ref, onMounted } from "vue";
import { Refresh } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { getSmsTaskList } from "../../../api/sms";
import { formatDateTime } from "@/utils/datetime";
const loading = ref(false);
+4 -1
View File
@@ -55,7 +55,9 @@
/>
</template>
</el-table-column>
<el-table-column prop="create_time" label="登记时间" width="170" />
<el-table-column prop="create_time" label="登记时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="scope">
<el-button size="small" text type="primary" @click="openEditor(scope.row)">
@@ -131,6 +133,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Upload, Refresh } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/datetime'
import {
getTemplateList,
scanTemplates,
@@ -97,11 +97,11 @@
</el-descriptions-item>
<el-descriptions-item label="创建时间" label-width="130px">
{{ detail.create_time || '—' }}
{{ detail.create_time ? formatDateTime(detail.create_time) : '—' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" label-width="130px">
{{ detail.update_time || '—' }}
{{ detail.update_time ? formatDateTime(detail.update_time) : '—' }}
</el-descriptions-item>
</el-descriptions>
</div>
@@ -124,6 +124,7 @@
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { getAgentApiDetail } from '@/api/agentapimanagement'
import { formatDateTime } from '@/utils/datetime'
import {
providerLabel,
providerColor,
@@ -203,7 +203,7 @@
<!-- 卡片底部 -->
<div class="card-foot">
<span class="foot-time">{{ row.create_time || '' }}</span>
<span class="foot-time">{{ row.create_time ? formatDateTime(row.create_time) : '' }}</span>
</div>
</div>
</div>
@@ -272,6 +272,7 @@ import {
import AgentApiEdit from './components/edit.vue'
import AgentApiDetail from './components/detail.vue'
import AgentApiTest from './components/test.vue'
import { formatDateTime } from '@/utils/datetime'
const editRef = ref(null)
const detailRef = ref(null)
+2 -5
View File
@@ -77,6 +77,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Delete, MoreFilled } from '@element-plus/icons-vue';
import NoteEditor from './components/edit.vue';
import { getNotebookList, deleteNotebook } from '@/api/notebook';
import { formatDate } from '@/utils/datetime';
const searchKeyword = ref('');
const currentNoteId = ref(null);
@@ -117,11 +118,7 @@ const formatTime = (dateStr) => {
} else if (diff < 7 * day) {
return `${Math.floor(diff / day)} 天前`;
} else {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
return formatDate(date);
}
};
+3 -10
View File
@@ -245,6 +245,8 @@ import {
} from "@element-plus/icons-vue";
import { getUserInfo, updateUserInfo } from "@/api/user";
import { useAuthStore } from "@/stores/auth";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
const router = useRouter();
const authStore = useAuthStore();
@@ -304,16 +306,7 @@ const passwordFormRules = {
]
};
const formatDate = (date) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const getSexText = (sex) => {
const map = {