chore: 整理项目配置与代码优化

1.  统一前端环境变量命名,新增通用项目标题配置
2.  修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析
3.  优化表格列排序、搜索过滤功能与页面布局样式
4.  重构路由与状态管理代码,移除循环依赖
5.  更新依赖版本与脚本命令,修复构建警告
6.  调整多语言文案与快捷入口配置
7.  修复通知中心样式与接口返回格式
This commit is contained in:
zhangtao
2026-07-26 23:33:17 +08:00
parent df00863b6c
commit 698e198280
78 changed files with 1142 additions and 1134 deletions
@@ -142,20 +142,14 @@ export default UserAPI;
export interface ForgetPasswordForm {
username: string;
new_password: string;
mobile?: string;
confirmPassword: string;
captcha_key?: string;
captcha?: string;
}
export interface RegisterForm {
username: string;
password: string;
confirmPassword: string;
email?: string;
name?: string;
captcha_key?: string;
captcha?: string;
}
export interface UserPageQuery extends PageQuery, UserByQueryParams {
@@ -29,36 +29,36 @@
<!-- 卡片网格 -->
<div v-else-if="!isEmpty" :style="{ padding: `0 ${gutter / 2}px` }">
<ElRow :gutter="gutter">
<ElCol
v-for="(item, index) in items"
:key="item[keyField] ?? index"
:xs="xs"
:sm="sm"
:md="md"
:lg="lg"
:xl="xl"
class="mb-4"
>
<ElCard
class="fa-card"
:class="cardClass"
shadow="hover"
:header-class="headerClass"
:body-class="bodyClass"
:footer-class="footerClass"
@click="(e: MouseEvent) => emit('itemClick', item, e)"
>
<template v-if="$slots.header" #header>
<slot name="header" :item="item" :index="index" />
</template>
<ElCol
v-for="(item, index) in items"
:key="item[keyField] ?? index"
:xs="xs"
:sm="sm"
:md="md"
:lg="lg"
:xl="xl"
class="mb-4"
>
<ElCard
class="fa-card"
:class="cardClass"
shadow="hover"
:header-class="headerClass"
:body-class="bodyClass"
:footer-class="footerClass"
@click="(e: MouseEvent) => emit('itemClick', item, e)"
>
<template v-if="$slots.header" #header>
<slot name="header" :item="item" :index="index" />
</template>
<slot :item="item" :index="index" />
<slot :item="item" :index="index" />
<template v-if="$slots.footer" #footer>
<slot name="footer" :item="item" :index="index" />
</template>
</ElCard>
</ElCol>
<template v-if="$slots.footer" #footer>
<slot name="footer" :item="item" :index="index" />
</template>
</ElCard>
</ElCol>
</ElRow>
</div>
@@ -86,7 +86,15 @@
<script setup lang="ts" generic="T extends Record<string, any>">
import { computed } from "vue";
import { ElCard, ElRow, ElCol, ElEmpty, ElSkeleton, ElSkeletonItem, ElScrollbar } from "element-plus";
import {
ElCard,
ElRow,
ElCol,
ElEmpty,
ElSkeleton,
ElSkeletonItem,
ElScrollbar,
} from "element-plus";
import FaPagination from "@/components/others/fa-pagination/index.vue";
defineOptions({ name: "FaCardGrid" });
@@ -35,7 +35,7 @@
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
@update:model-value="
(v: number | undefined) => {
modelValue.value['created_id'] = v;
modelValue['created_id'] = v;
}
"
@confirm-click="emitImmediateSearch"
@@ -50,7 +50,7 @@
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
@update:model-value="
(v: number | undefined) => {
modelValue.value['updated_id'] = v;
modelValue['updated_id'] = v;
}
"
@confirm-click="emitImmediateSearch"
@@ -124,6 +124,7 @@ import { resolveIconForFaSvgIcon } from "@utils";
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { MagicStick, Position, Tools } from "@element-plus/icons-vue";
import { useSettingsStore } from "@stores";
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
@@ -25,7 +25,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.second.specified" :disabled="fields.second.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.second.specified"
:disabled="fields.second.mode !== 'specify'"
>
<ElCheckbox v-for="v in secondOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -57,7 +60,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.minute.specified" :disabled="fields.minute.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.minute.specified"
:disabled="fields.minute.mode !== 'specify'"
>
<ElCheckbox v-for="v in minuteOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -89,7 +95,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.hour.specified" :disabled="fields.hour.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.hour.specified"
:disabled="fields.hour.mode !== 'specify'"
>
<ElCheckbox v-for="v in hourOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -130,7 +139,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.day.specified" :disabled="fields.day.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.day.specified"
:disabled="fields.day.mode !== 'specify'"
>
<ElCheckbox v-for="v in dayOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -163,7 +175,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.month.specified" :disabled="fields.month.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.month.specified"
:disabled="fields.month.mode !== 'specify'"
>
<ElCheckbox v-for="v in monthOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -200,7 +215,10 @@
<ElRadio value="specify">
<div class="flex items-center">
<span class="mr-2">指定</span>
<ElCheckboxGroup v-model="fields.week.specified" :disabled="fields.week.mode !== 'specify'">
<ElCheckboxGroup
v-model="fields.week.specified"
:disabled="fields.week.mode !== 'specify'"
>
<ElCheckbox v-for="v in weekOptions" :key="v" :label="v" :value="v" />
</ElCheckboxGroup>
</div>
@@ -220,266 +238,293 @@
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-(--el-text-color-secondary)">下次执行</span>
<span class="text-sm">{{ nextRun || '—' }}</span>
<span class="text-sm">{{ nextRun || "—" }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import cronParser from 'cron-parser'
import dayjs from 'dayjs'
import { computed, nextTick, ref, watch } from "vue";
import cronParser from "cron-parser";
import dayjs from "dayjs";
interface FieldState {
mode: string
rangeStart: string
rangeEnd: string
stepFrom: string
stepInterval: string
specified: string[]
extra: string
mode: string;
rangeStart: string;
rangeEnd: string;
stepFrom: string;
stepInterval: string;
specified: string[];
extra: string;
}
interface Fields {
second: FieldState
minute: FieldState
hour: FieldState
day: FieldState
month: FieldState
week: FieldState
second: FieldState;
minute: FieldState;
hour: FieldState;
day: FieldState;
month: FieldState;
week: FieldState;
}
const defaultField = (init: Partial<FieldState> = {}): FieldState => ({
mode: 'every',
rangeStart: '0',
rangeEnd: '0',
stepFrom: '0',
stepInterval: '1',
mode: "every",
rangeStart: "0",
rangeEnd: "0",
stepFrom: "0",
stepInterval: "1",
specified: [],
extra: '',
extra: "",
...init,
})
});
const fields = ref<Fields>({
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
})
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
});
const activeTab = ref('秒')
const activeTab = ref("秒");
const props = defineProps<{ modelValue?: string }>()
const props = defineProps<{ modelValue?: string }>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
"update:modelValue": [value: string];
}>();
const pad = (n: number) => n.toString().padStart(2, '0')
const rangeOptions = (count: number, start = 0) => Array.from({ length: count }, (_, i) => pad(start + i))
const secondOptions = rangeOptions(60)
const minuteOptions = rangeOptions(60)
const hourOptions = rangeOptions(24)
const dayOptions = rangeOptions(31, 1)
const monthOptions = rangeOptions(12, 1)
const weekOptions = rangeOptions(7, 1)
const pad = (n: number) => n.toString().padStart(2, "0");
const rangeOptions = (count: number, start = 0) =>
Array.from({ length: count }, (_, i) => pad(start + i));
const secondOptions = rangeOptions(60);
const minuteOptions = rangeOptions(60);
const hourOptions = rangeOptions(24);
const dayOptions = rangeOptions(31, 1);
const monthOptions = rangeOptions(12, 1);
const weekOptions = rangeOptions(7, 1);
function toSegment(field: FieldState): string {
switch (field.mode) {
case 'every':
return '*'
case 'any':
return '?'
case 'range':
return `${field.rangeStart}-${field.rangeEnd}`
case 'step':
return `${field.stepFrom}/${field.stepInterval}`
case 'specify':
return field.specified.map((v) => parseInt(v)).sort((a, b) => a - b).join(',') || '*'
case 'last':
return 'L'
case 'lastWeekday':
return `${field.extra}W`
case 'lastWeek':
return `${field.extra}L`
case 'nthWeek':
return `${field.rangeStart}#${field.rangeEnd}`
case "every":
return "*";
case "any":
return "?";
case "range":
return `${field.rangeStart}-${field.rangeEnd}`;
case "step":
return `${field.stepFrom}/${field.stepInterval}`;
case "specify":
return (
field.specified
.map((v) => parseInt(v))
.sort((a, b) => a - b)
.join(",") || "*"
);
case "last":
return "L";
case "lastWeekday":
return `${field.extra}W`;
case "lastWeek":
return `${field.extra}L`;
case "nthWeek":
return `${field.rangeStart}#${field.rangeEnd}`;
default:
return '*'
return "*";
}
}
const cron = computed(() => {
const f = fields.value
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`
})
const f = fields.value;
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`;
});
const cronDesc = computed(() => {
const { second, minute, hour, day, month, week } = fields.value
const W = ['', '周日', '周一', '周二', '周三', '周四', '周五', '周六']
const { second, minute, hour, day, month, week } = fields.value;
const W = ["", "周日", "周一", "周二", "周三", "周四", "周五", "周六"];
// 层级: 年 -> 月 -> 日/周 -> 时 -> 分 -> 秒
const parts: string[] = []
parts.push('每年')
const parts: string[] = [];
parts.push("每年");
// 月
if (month.mode === 'specify' && month.specified.length) parts.push(`${month.specified.join('、')}`)
else parts.push('每月')
if (month.mode === "specify" && month.specified.length)
parts.push(`${month.specified.join("、")}`);
else parts.push("每月");
// 日/周
if (day.mode === 'specify') parts.push(`${day.specified.join('、')}`)
else if (day.mode === 'last') parts.push('最后一天')
else if (day.mode === 'lastWeekday') parts.push(`${day.extra}号最近工作日`)
else if (week.mode === 'specify') parts.push(week.specified.map((v) => W[+v] || v).join('、'))
else if (week.mode === 'range') parts.push(`${W[+week.rangeStart] || ''}${W[+week.rangeEnd] || ''}`)
else if (week.mode === 'lastWeek') parts.push(`最后一个${W[+week.extra] || ''}`)
else if (week.mode === 'every') parts.push(day.mode === 'any' ? '每周' : '每天')
else parts.push('每天')
if (day.mode === "specify") parts.push(`${day.specified.join("、")}`);
else if (day.mode === "last") parts.push("最后一天");
else if (day.mode === "lastWeekday") parts.push(`${day.extra}号最近工作日`);
else if (week.mode === "specify") parts.push(week.specified.map((v) => W[+v] || v).join("、"));
else if (week.mode === "range")
parts.push(`${W[+week.rangeStart] || ""}${W[+week.rangeEnd] || ""}`);
else if (week.mode === "lastWeek") parts.push(`最后一个${W[+week.extra] || ""}`);
else if (week.mode === "every") parts.push(day.mode === "any" ? "每周" : "每天");
else parts.push("每天");
// 时
if (hour.mode === 'every') parts.push('每时')
else if (hour.mode === 'step') parts.push(`${hour.stepInterval}`)
else if (hour.mode === 'specify' && hour.specified.length === 1) parts.push(`${hour.specified[0]}`)
if (hour.mode === "every") parts.push("每时");
else if (hour.mode === "step") parts.push(`${hour.stepInterval}`);
else if (hour.mode === "specify" && hour.specified.length === 1)
parts.push(`${hour.specified[0]}`);
// 分
if (minute.mode === 'every') parts.push('每分')
else if (minute.mode === 'step') parts.push(`${minute.stepInterval}`)
else if (minute.mode === 'specify' && minute.specified.length === 1) parts.push(`${minute.specified[0]}`)
if (minute.mode === "every") parts.push("每分");
else if (minute.mode === "step") parts.push(`${minute.stepInterval}`);
else if (minute.mode === "specify" && minute.specified.length === 1)
parts.push(`${minute.specified[0]}`);
// 秒
if (second.mode === 'every') parts.push('每秒')
else if (second.mode === 'step') parts.push(`${second.stepInterval}`)
else if (second.mode === 'specify' && second.specified.length === 1) parts.push(`${second.specified[0]}`)
if (second.mode === "every") parts.push("每秒");
else if (second.mode === "step") parts.push(`${second.stepInterval}`);
else if (second.mode === "specify" && second.specified.length === 1)
parts.push(`${second.specified[0]}`);
return parts.join(' ')
})
return parts.join(" ");
});
let skipParse = false
let skipParse = false;
watch(cron, (val) => {
skipParse = true
emit('update:modelValue', val)
nextTick(() => { skipParse = false })
})
skipParse = true;
emit("update:modelValue", val);
nextTick(() => {
skipParse = false;
});
});
// ---- 解析 cron 表达式 → 回填 fields ----
function parseSegment(seg: string, field: FieldState): void {
if (seg === '*') {
field.mode = 'every'
} else if (seg === '?') {
field.mode = 'any'
} else if (seg === 'L') {
field.mode = 'last'
} else if (seg.includes('W')) {
field.mode = 'lastWeekday'
field.extra = seg.replace('W', '')
} else if (seg.includes('L')) {
field.mode = 'lastWeek'
field.extra = seg.replace('L', '')
} else if (seg.includes('#')) {
field.mode = 'nthWeek'
const [a, b] = seg.split('#')
field.rangeStart = a!
field.rangeEnd = b!
} else if (seg.includes('-')) {
field.mode = 'range'
const [a, b] = seg.split('-')
field.rangeStart = a!
field.rangeEnd = b!
} else if (seg.includes('/')) {
field.mode = 'step'
const [a, b] = seg.split('/')
field.stepFrom = a!
field.stepInterval = b!
} else if (seg.includes(',')) {
field.mode = 'specify'
field.specified = seg.split(',').map((v) => pad(parseInt(v)))
if (seg === "*") {
field.mode = "every";
} else if (seg === "?") {
field.mode = "any";
} else if (seg === "L") {
field.mode = "last";
} else if (seg.includes("W")) {
field.mode = "lastWeekday";
field.extra = seg.replace("W", "");
} else if (seg.includes("L")) {
field.mode = "lastWeek";
field.extra = seg.replace("L", "");
} else if (seg.includes("#")) {
field.mode = "nthWeek";
const [a, b] = seg.split("#");
field.rangeStart = a!;
field.rangeEnd = b!;
} else if (seg.includes("-")) {
field.mode = "range";
const [a, b] = seg.split("-");
field.rangeStart = a!;
field.rangeEnd = b!;
} else if (seg.includes("/")) {
field.mode = "step";
const [a, b] = seg.split("/");
field.stepFrom = a!;
field.stepInterval = b!;
} else if (seg.includes(",")) {
field.mode = "specify";
field.specified = seg.split(",").map((v) => pad(parseInt(v)));
} else if (/^\d+$/.test(seg)) {
field.mode = 'specify'
field.specified = [pad(parseInt(seg))]
field.mode = "specify";
field.specified = [pad(parseInt(seg))];
}
}
function applyCron(expr: string) {
const parts = expr.trim().split(/\s+/)
if (parts.length < 6) return
const keys: (keyof Fields)[] = ['second', 'minute', 'hour', 'day', 'month', 'week']
const parts = expr.trim().split(/\s+/);
if (parts.length < 6) return;
const keys: (keyof Fields)[] = ["second", "minute", "hour", "day", "month", "week"];
keys.forEach((key, i) => {
parseSegment(parts[i]!, fields.value[key])
})
parseSegment(parts[i]!, fields.value[key]);
});
// 自动定位到第一个非默认的 tab
const idx = parts.findIndex((p) => p !== '*' && p !== '?')
const tabMap = ['秒', '分钟', '小时', '日', '月', '周']
activeTab.value = idx >= 0 ? tabMap[idx]! : '秒'
const idx = parts.findIndex((p) => p !== "*" && p !== "?");
const tabMap = ["秒", "分钟", "小时", "日", "月", "周"];
activeTab.value = idx >= 0 ? tabMap[idx]! : "秒";
}
watch(() => props.modelValue, (val) => {
if (skipParse) return
if (val) applyCron(val)
}, { immediate: true })
watch(
() => props.modelValue,
(val) => {
if (skipParse) return;
if (val) applyCron(val);
},
{ immediate: true }
);
// ---- 下一次执行时间 ----
const nextRun = ref('')
const nextRun = ref("");
function calcNextRun(expr: string) {
if (!expr?.trim()) { nextRun.value = ''; return }
if (!expr?.trim()) {
nextRun.value = "";
return;
}
try {
const parser = (cronParser as any).default ?? cronParser
const parser = (cronParser as any).default ?? cronParser;
const interval = parser.parse(expr, {
currentDate: new Date(),
})
nextRun.value = dayjs(interval.next().toDate()).format('YYYY-MM-DD HH:mm:ss')
});
nextRun.value = dayjs(interval.next().toDate()).format("YYYY-MM-DD HH:mm:ss");
} catch {
nextRun.value = '无效表达式'
nextRun.value = "无效表达式";
}
}
watch(cron, (val) => calcNextRun(val), { immediate: true })
watch(cron, (val) => calcNextRun(val), { immediate: true });
const clear = () => {
fields.value = {
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
}
activeTab.value = '秒'
}
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
};
activeTab.value = "秒";
};
defineExpose({ clear })
defineExpose({ clear });
</script>
<style lang="scss" scoped>
.cron-selector {
padding: 12px;
:deep(.el-tabs__content) { padding: 8px; }
:deep(.el-tabs__content) {
padding: 8px;
}
:deep(.el-radio-group) {
.el-radio {
width: 100%;
height: auto;
margin-bottom: 6px;
min-height: 28px;
margin-bottom: 6px;
}
}
:deep(.el-checkbox-group) {
display: flex;
flex-wrap: wrap;
.el-checkbox { margin-right: 6px; height: auto; margin-bottom: 6px; }
.el-checkbox {
height: auto;
margin-right: 6px;
margin-bottom: 6px;
}
}
}
.cron-preview {
margin-top: 12px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
margin-top: 12px;
background-color: var(--el-fill-color);
border-radius: 4px;
}
+18 -19
View File
@@ -11,22 +11,22 @@ const fastEnterConfig: FastEnterConfig = {
// 应用列表
applications: [
{
name: "功能引导",
description: "产品操作指南",
icon: "ri:compass-3-line",
iconColor: "#009688",
enabled: true,
order: 1,
routeName: "FastlinkTutorial",
},
{
name: "文章列表",
description: "文章管理与查看",
icon: "ri:article-line",
name: "用户管理",
description: "系统用户管理与维护",
icon: "ri:user-settings-line",
iconColor: "#377dff",
enabled: true,
order: 1,
routeName: "User",
},
{
name: "角色管理",
description: "角色权限配置与分配",
icon: "ri:shield-user-line",
iconColor: "#FF6B35",
enabled: true,
order: 2,
routeName: "FastlinkArticleList",
routeName: "Role",
},
{
name: "定价",
@@ -49,7 +49,7 @@ const fastEnterConfig: FastEnterConfig = {
{
name: "官方文档",
description: "使用指南与开发文档",
icon: "ri:bill-line",
icon: "ri:book-open-line",
iconColor: "#ffb100",
enabled: true,
order: 5,
@@ -110,17 +110,16 @@ const fastEnterConfig: FastEnterConfig = {
isDialog: true,
},
{
name: "个人中心",
name: "操作日志",
enabled: true,
order: 5,
routeName: "FastlinkProfile",
routeName: "Log",
},
{
name: "留言管理",
name: "个人中心",
enabled: true,
order: 6,
routeName: "FastlinkArticleList",
routeQuery: { commentWall: "1" },
routeName: "FastlinkProfile",
},
],
};
+2 -2
View File
@@ -34,10 +34,10 @@ export function useSiteConfig() {
if (favicon?.config_value) updateFavicon(favicon.config_value);
};
/** 初始化:强制拉取配置同步标题/favicon */
/** 初始化:优先使用缓存配置同步标题/favicon,无缓存时从接口获取 */
const initSiteConfig = async () => {
try {
await configStore.getConfig(true);
await configStore.getConfig();
applyConfig();
} catch (error) {
console.error("[SiteConfig] 获取配置失败:", error);
@@ -278,6 +278,7 @@ const { isFullscreen, toggle: toggleFullscreen } = useFullscreen();
onMounted(() => {
initLanguage();
document.addEventListener("click", bodyCloseNotice);
noticeStore.getNotice();
});
onUnmounted(() => {
@@ -21,8 +21,11 @@
class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
@click="handleMarkAsRead(index)"
>
<div class="size-9 leading-9 text-center rounded-lg flex-cc bg-theme/12 text-theme">
<FaSvgIcon class="text-lg bg-transparent!" icon="ri:notification-3-line" />
<div
class="size-9 leading-9 text-center rounded-lg flex-cc"
:class="item.type === 2 ? 'bg-warning/12 text-warning' : 'bg-theme/12 text-theme'"
>
<FaSvgIcon class="text-lg bg-transparent!" :icon="getNoticeIcon(item.type)" />
</div>
<div class="w-[calc(100%-45px)] ml-3.5">
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
@@ -58,10 +61,13 @@ import NoticeAPI from "@/api/module_system/notice";
defineOptions({ name: "FaNotification" });
const router = useRouter();
interface NoticeItem {
title: string;
time: string;
read: boolean;
type: number;
}
interface Props {
@@ -81,6 +87,9 @@ const visible = ref(false);
const noticeList = ref<NoticeItem[]>([]);
const loading = ref(false);
const getNoticeIcon = (type: number) =>
type === 2 ? "ri:megaphone-line" : "ri:notification-3-line";
const fetchNotices = async () => {
loading.value = true;
try {
@@ -90,6 +99,7 @@ const fetchNotices = async () => {
title: n.notice_title ?? "",
time: n.created_time ?? "",
read: false,
type: Number(n.notice_type) || 1,
}));
} catch {
noticeList.value = [];
@@ -104,8 +114,7 @@ watch(visible, (v) => {
});
const handleViewAll = () => {
const router = useRouter();
router.push("/module_system/notice");
router.push("/system/notice");
emit("update:value", false);
};
+2 -2
View File
@@ -369,7 +369,7 @@
"user": "User"
},
"placeholder": {
"username": "Please enter username",
"username": "Please enter account",
"password": "Please enter password",
"email": "Please enter email",
"slider": "Please slide to verify"
@@ -473,7 +473,7 @@
"username": "Please enter account",
"password": "Please enter password",
"confirmPassword": "Please enter password again",
"email": "Please enter email"
"name": "Please enter nickname"
},
"rule": {
"confirmPasswordRequired": "Please enter your password again",
+2 -2
View File
@@ -369,7 +369,7 @@
"user": "普通用户"
},
"placeholder": {
"username": "请输入用户名",
"username": "请输入账号",
"password": "请输入密码",
"email": "请输入邮箱",
"slider": "请拖动滑块完成验证"
@@ -473,7 +473,7 @@
"username": "请输入账号",
"password": "请输入密码",
"confirmPassword": "请再次输入密码",
"email": "请输入邮箱"
"name": "请输入昵称"
},
"rule": {
"confirmPasswordRequired": "请再次输入密码",
+6 -6
View File
@@ -294,12 +294,6 @@ function getDashboardMenuTreeForMerge(): AppRouteRecord {
};
}
const mergeShellHomeMenu: AppRouteRecord = {
path: "/home",
name: "Home",
meta: { ...HOME_MENU_META, shellRoute: true },
};
function normalizeMenuPath(path?: string): string {
if (!path || !path.trim()) return "";
const p = path.trim();
@@ -347,6 +341,12 @@ export function mergeShellRoutesIntoMenu(menuList: AppRouteRecord[]): AppRouteRe
}
};
const mergeShellHomeMenu: AppRouteRecord = {
path: "/home",
name: "Home",
meta: { ...HOME_MENU_META, shellRoute: true },
};
tryPush(mergeShellHomeMenu);
if (!paths.has("/dashboard")) {
tryPush(dashboardRoutesToShellMenu(getDashboardMenuTreeForMerge()));
+10 -69
View File
@@ -13,8 +13,10 @@ import { useUserStore, useMenuStore, useWorktabStore } from "@/store";
import { IframeRouteManager, ROUTE_PATH_LOGIN_ALT } from "./routes";
import { router, HOME_PAGE_PATH } from "./index";
import { setPageTitle, setWorktab } from "@utils/navigation";
import { MenuProcessor } from "./MenuProcessor";
import { NProgress } from "@utils/ui";
import { Auth } from "@utils/auth";
import { refreshState } from "./refresh";
/** 全局 loading 状态(用于路由切换时显示加载遮罩) */
const globalLoading = ref(false);
@@ -38,36 +40,12 @@ function isAnonymousPublicPath(path: string): boolean {
function isLoginRoute(to: RouteLocationNormalized): boolean {
return to.path === "/login" || to.path === ROUTE_PATH_LOGIN_ALT;
}
// ──────── 守卫状态 ────────
let pendingLoading = false;
let routeInitFailed = false;
/** 获取是否正在加载动态路由 */
export function getPendingLoading(): boolean {
return pendingLoading;
}
/** 重置加载状态 */
export function resetPendingLoading(): void {
pendingLoading = false;
}
/** 获取路由初始化是否失败 */
export function getRouteInitFailed(): boolean {
return routeInitFailed;
}
/** 重置路由初始化失败状态 */
export function resetRouteInitState(): void {
routeInitFailed = false;
}
// ──────── 前置守卫 ────────
/**
* 前置守卫:导航前检查登录状态、动态路由加载、权限校验
*/
export function setupBeforeEachGuard(router: Router): void {
let dynamicRoutesRegistered = false;
router.beforeEach(async (to) => {
// 初始化守卫状态
if (globalLoading.value) globalLoading.value = false;
@@ -86,19 +64,19 @@ export function setupBeforeEachGuard(router: Router): void {
}
// 路由初始化失败 → 跳转 500
if (routeInitFailed && !isAnonymousPublicPath(to.path)) {
if (refreshState.routeInitFailed && !isAnonymousPublicPath(to.path)) {
return "/500";
}
// 已登录、动态路由未注册 → 加载
if (!dynamicRoutesRegistered && !isAnonymousPublicPath(to.path)) {
if (!refreshState.dynamicRoutesRegistered && !isAnonymousPublicPath(to.path)) {
// 正在加载中 → 跳转首页,避免进入后路由未注册导致 404
if (pendingLoading) {
if (refreshState.pendingLoading) {
return { path: HOME_PAGE_PATH, replace: true };
}
const redirect = await handleDynamicRoutes(to);
if (redirect) return redirect;
dynamicRoutesRegistered = true;
refreshState.dynamicRoutesRegistered = true;
// 注册动态路由后,若原导航被 catch-all 404 捕获(F5 刷新场景),重定向触发重新解析
if (to.matched.some((r) => r.name === "CatchAll404")) {
@@ -152,8 +130,8 @@ async function handleLoginStatus(to: RouteLocationNormalized): Promise<boolean>
async function handleDynamicRoutes(
to: RouteLocationNormalized
): Promise<undefined | string | { path: string; replace: boolean }> {
if (pendingLoading) return;
pendingLoading = true;
if (refreshState.pendingLoading) return;
refreshState.pendingLoading = true;
try {
// 异常恢复:菜单空了但路由还在,做反注册
@@ -166,7 +144,6 @@ async function handleDynamicRoutes(
}
// 获取菜单
const { MenuProcessor } = await import("./MenuProcessor");
const menuProcessor = new MenuProcessor();
const menuList = await menuProcessor.getMenuList();
@@ -206,10 +183,10 @@ async function handleDynamicRoutes(
return undefined;
} catch (error) {
console.error("[路由守卫] 路由初始化失败:", error);
routeInitFailed = true;
refreshState.routeInitFailed = true;
return "/500";
} finally {
pendingLoading = false;
refreshState.pendingLoading = false;
}
}
@@ -319,42 +296,6 @@ export class RoutePermissionValidator {
}
}
// ──────── 动态路由卸载 ────────
/** 卸载动态路由 + 清理 iframe 路由(退出登录时调用) */
export async function resetDynamicRoutesSync(): Promise<void> {
const menuStore = useMenuStore();
const removeRouteFns = menuStore.removeRouteFns;
removeRouteFns.forEach((fn: () => void) => fn());
menuStore.menuList.length = 0;
menuStore.removeRouteFns.length = 0;
IframeRouteManager.getInstance().clear();
}
/** 重新拉菜单 + 重新注册(管理员手动刷新菜单时调用) */
export async function refreshMenuAndRoutes(): Promise<void> {
await resetDynamicRoutesSync();
const { MenuProcessor } = await import("./MenuProcessor");
const { RouteRegistry } = await import("./route-loader");
const menuProcessor = new MenuProcessor();
const menuList = await menuProcessor.getMenuList();
// 更新侧栏菜单并注册动态路由
useMenuStore().setMenuList(menuList);
const routeRegistry = new RouteRegistry(router);
routeRegistry.register(menuList);
useMenuStore().addRemoveRouteFns(routeRegistry.getRemoveRouteFns());
// 菜单变更后清理无效的持久化标签
useWorktabStore().validateWorktabs(router);
}
/** 延迟重置(token 过期降级时使用 3000ms 等待过渡动画) */
export async function resetRouterState(delay: number = 0): Promise<void> {
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
await resetDynamicRoutesSync();
}
// ──────── 后置守卫 ────────
/**
+1 -4
View File
@@ -1,7 +1,7 @@
import type { App } from "vue";
import { createRouter, createWebHashHistory } from "vue-router";
import { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME, staticRoutes } from "./routes";
import { setupAfterEachGuard } from "./guards";
import { setupAfterEachGuard, setupBeforeEachGuard } from "./guards";
import "@utils/ui";
/**
@@ -21,7 +21,6 @@ export const router = createRouter({
/** 注册守卫 + 挂载 router 到 Vue app(在 main.ts 调用) */
export async function initRouter(app: App<Element>): Promise<void> {
const { setupBeforeEachGuard } = await import("./guards");
setupBeforeEachGuard(router);
setupAfterEachGuard(router);
app.use(router);
@@ -32,8 +31,6 @@ export const HOME_PAGE_PATH = "/home";
export { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME };
/** 动态路由注册相关类(从 `@/router` 按需导入) */
export { RouteRegistry, ComponentLoader, RouteTransformer } from "./route-loader";
/** iframe 路由管理器 */
export { IframeRouteManager } from "./routes";
/** 菜单处理(获取、过滤、壳层补全) */
+59
View File
@@ -0,0 +1,59 @@
/**
* 路由刷新/重置工具函数
*
* guards.ts 和 store/index.ts 共享的路由操作模块。
* router 实例由调用方作为参数传入,存储通过静态导入获取。
*
* @module router/refresh
*/
import type { Router } from "vue-router";
import { useMenuStore, useWorktabStore } from "@/store";
import { MenuProcessor } from "./MenuProcessor";
import { IframeRouteManager } from "./routes";
// ──────── 守卫状态 ────────
export const refreshState = {
pendingLoading: false,
routeInitFailed: false,
dynamicRoutesRegistered: false,
};
export function resetRouteInitState(): void {
refreshState.routeInitFailed = false;
refreshState.dynamicRoutesRegistered = false;
}
// ──────── 路由操作 ────────
/** 异步清理已注册的动态路由和菜单 */
export async function resetDynamicRoutesSync(): Promise<void> {
const menuStore = useMenuStore();
const removeRouteFns = menuStore.removeRouteFns;
removeRouteFns.forEach((fn: () => void) => fn());
menuStore.menuList.length = 0;
menuStore.removeRouteFns.length = 0;
IframeRouteManager.getInstance().clear();
}
/** 重新拉菜单 + 重新注册(管理员手动刷新菜单时调用) */
export async function refreshMenuAndRoutes(router: Router): Promise<void> {
await resetDynamicRoutesSync();
const { RouteRegistry } = await import("./route-loader");
const menuProcessor = new MenuProcessor();
const menuList = await menuProcessor.getMenuList();
// 更新侧栏菜单并注册动态路由
useMenuStore().setMenuList(menuList);
const routeRegistry = new RouteRegistry(router);
routeRegistry.register(menuList);
useMenuStore().addRemoveRouteFns(routeRegistry.getRemoveRouteFns());
// 菜单变更后清理无效的持久化标签
useWorktabStore().validateWorktabs(router);
}
/** 延迟重置(token 过期降级时使用 3000ms 等待过渡动画) */
export async function resetRouterState(delay: number = 0): Promise<void> {
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
await resetDynamicRoutesSync();
}
+7 -11
View File
@@ -13,6 +13,7 @@ import type { AppRouteRecord } from "@/types/router";
import { h } from "vue";
import {
IframeRouteManager,
IframeView,
NestedRouterParent,
ROOT_LAYOUT_ROUTE_NAME,
ROUTE_COMPONENT_LAYOUT,
@@ -21,8 +22,8 @@ import {
// ──────── ComponentLoader ────────
/** 页面组件映射表(eager 加载:src/views 下所有 .vue 文件) */
const pageComponents = import.meta.glob("/src/views/**/*.vue", { eager: true });
/** 页面组件映射表(eager 加载:src/views 及 layouts 下所有 .vue 文件) */
const pageComponents = import.meta.glob("/src/{views,layouts}/**/*.vue", { eager: true });
/**
* 组件加载器
@@ -54,17 +55,12 @@ export class ComponentLoader {
/** Layout 框架组件 */
loadLayout(): any {
const mod = pageComponents["/src/layouts/index.vue"];
return mod ? (mod as any).default : null;
return (pageComponents["/src/layouts/index.vue"] as any)?.default;
}
/** IframeView 组件 */
loadIframe(): any {
return () =>
import("./routes").then((m) => {
const IframeView = (m as any).default || (m as any).IframeView;
return IframeView;
});
return IframeView;
}
/** NestedRouterParent 占位组件 */
@@ -203,7 +199,7 @@ export class RouteTransformer {
return {
path: `/${firstSegment}`,
name: route.name || firstSegment,
component: this.loader.loadLayout() || (() => import("@/layouts/index.vue")),
component: this.loader.loadLayout(),
meta: { title: route.meta?.title, icon: route.meta?.icon },
redirect: route.path.startsWith("/") ? route.path : `/${route.path}`,
children: [
@@ -231,7 +227,7 @@ export class RouteTransformer {
return {
path: `/${firstSegment}`,
name: route.name || firstSegment,
component: this.loader.loadLayout() || (() => import("@/layouts/index.vue")),
component: this.loader.loadLayout(),
meta: { title: route.meta?.title, icon: route.meta?.icon },
redirect: fullMenuPath,
children: [
+36 -23
View File
@@ -13,6 +13,22 @@ import type { AppRouteRecord, RouteMeta } from "@/types/router";
import { defineComponent, h, onMounted, ref } from "vue";
import { RouterView, useRoute } from "vue-router";
import { $t } from "@/locales";
import LayoutComponent from "@/layouts/index.vue";
import DashboardWorkplace from "@views/dashboard/workplace/index.vue";
import DashboardAnalysis from "@views/dashboard/analysis/index.vue";
import DashboardScreen from "@views/dashboard/screen/index.vue";
import RedirectView from "@views/redirect/index.vue";
import LoginView from "@views/module_system/auth/login/index.vue";
import Exception401 from "@views/exception/401/index.vue";
import Exception403 from "@views/exception/403/index.vue";
import Exception404 from "@views/exception/404/index.vue";
import Exception500 from "@views/exception/500/index.vue";
import DashboardHome from "@views/dashboard/home/index.vue";
import FastlinkProfile from "@views/fastlink/current/profile.vue";
import FastlinkChangelog from "@views/fastlink/changelog/index.vue";
import FastlinkPricing from "@views/fastlink/pricing/index.vue";
import FastlinkTutorial from "@views/fastlink/tutorial/index.vue";
import FastlinkFachat from "@views/fastlink/fachat/index.vue";
// ──────── IframeRouteManager ────────
@@ -101,13 +117,13 @@ export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
{
path: "workplace",
name: "DashboardWorkplace",
component: () => import("@views/dashboard/workplace/index.vue"),
component: DashboardWorkplace,
meta: { title: "menus.dashboard.workplace", icon: "ri:bar-chart-box-line", keepAlive: true },
},
{
path: "analysis",
name: "DashboardAnalysis",
component: () => import("@views/dashboard/analysis/index.vue"),
component: DashboardAnalysis,
meta: {
title: "menus.dashboard.analysis",
icon: "ri:align-item-bottom-line",
@@ -117,7 +133,7 @@ export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
{
path: "screen",
name: "DashboardScreen",
component: () => import("@views/dashboard/screen/index.vue"),
component: DashboardScreen,
meta: { title: "数据大屏", icon: "ri:tv-line", keepAlive: false, hidden: false },
},
];
@@ -147,13 +163,10 @@ export const ROUTE_COMPONENT_NESTED_PARENT = "/nested/router-view-parent";
/** 登录页的备用 path(守卫判断用) */
export const ROUTE_PATH_LOGIN_ALT = "/auth/login";
/** 主框架 Layout 懒加载 */
export const Layout = () => import("@/layouts/index.vue");
// ──────── IframeView 组件 ────────
/** iframe 子路由的 Vue 组件 —— 从 IframeRouteManager 获取链接,加载时显示 loading */
const IframeView = defineComponent({
export const IframeView = defineComponent({
name: "IframeView",
setup() {
const route = useRoute();
@@ -199,11 +212,11 @@ export const staticRoutes: AppRouteRecordRaw[] = [
{
path: "/redirect",
meta: { hidden: true },
component: Layout,
component: LayoutComponent,
children: [
{
path: "/redirect/:path(.*)",
component: () => import("@views/redirect/index.vue"),
component: RedirectView,
},
],
},
@@ -212,44 +225,44 @@ export const staticRoutes: AppRouteRecordRaw[] = [
path: "/login",
name: "Login",
meta: { hidden: true, isHideTab: true, title: "menus.login.title" },
component: () => import("@views/module_system/auth/login/index.vue"),
component: LoginView,
},
// 异常页
{
path: "/401",
name: "401",
meta: { hidden: true, title: "401" },
component: () => import("@views/exception/401/index.vue"),
component: Exception401,
},
{
path: "/403",
name: "403",
component: () => import("@views/exception/403/index.vue"),
component: Exception403,
meta: { hidden: true, title: "403" },
},
{
path: "/404",
name: "404",
meta: { hidden: true, title: "404" },
component: () => import("@views/exception/404/index.vue"),
component: Exception404,
},
{
path: "/500",
name: "500",
meta: { hidden: true, title: "500" },
component: () => import("@views/exception/500/index.vue"),
component: Exception500,
},
// 根 Layout:存放壳层路由(home/dashboard/fastlink
{
path: "/",
name: ROOT_LAYOUT_ROUTE_NAME,
redirect: "/home",
component: Layout,
component: LayoutComponent,
children: [
{
path: "home",
name: HOME_ROUTE_NAME,
component: () => import("@views/dashboard/home/index.vue"),
component: DashboardHome,
meta: HOME_MENU_META,
},
{
@@ -271,7 +284,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
path: "profile",
name: "FastlinkProfile",
meta: { title: $t("menus.system.userCenter"), icon: "ri:user-line", hidden: true },
component: () => import("@views/fastlink/current/profile.vue"),
component: FastlinkProfile,
},
{
path: "changelog",
@@ -283,7 +296,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
keepAlive: true,
isHideTab: true,
},
component: () => import("@views/fastlink/changelog/index.vue"),
component: FastlinkChangelog,
},
{
path: "pricing",
@@ -295,7 +308,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
keepAlive: true,
isHideTab: true,
},
component: () => import("@views/fastlink/pricing/index.vue"),
component: FastlinkPricing,
},
{
path: "tutorial",
@@ -307,7 +320,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
keepAlive: true,
isHideTab: true,
},
component: () => import("@views/fastlink/tutorial/index.vue"),
component: FastlinkTutorial,
},
{
path: "fachat",
@@ -319,7 +332,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
keepAlive: true,
isHideTab: true,
},
component: () => import("@views/fastlink/fachat/index.vue"),
component: FastlinkFachat,
},
],
},
@@ -328,7 +341,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
// iframe 外部链接
{
path: "/outside",
component: () => import("@/layouts/index.vue"),
component: LayoutComponent,
name: "Outside",
meta: { title: "menus.outside.title" },
children: [
@@ -344,7 +357,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
{
path: "/:pathMatch(.*)*",
name: "CatchAll404",
component: () => import("@views/exception/404/index.vue"),
component: Exception404,
meta: { hidden: true, title: "404" },
},
];
+4 -3
View File
@@ -1,6 +1,8 @@
import type { App } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { router } from "@/router";
import { refreshMenuAndRoutes } from "@/router/refresh";
import { useUserStore } from "./modules/user.store";
import { useDictStore } from "./modules/dict.store";
import { useNoticeStore } from "./modules/notice.store";
@@ -62,7 +64,7 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
tasks.push(configStore.getConfig(true));
}
if (refreshNotice) {
tasks.push(noticeStore.getNotice());
tasks.push(noticeStore.getNotice(true));
}
if (dictTypes && dictTypes.length > 0) {
if (clearDictBefore) dictStore.clearDictData();
@@ -72,8 +74,7 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
await Promise.allSettled(tasks);
if (refreshRoutes) {
const { refreshMenuAndRoutes } = await import("@/router/guards");
await refreshMenuAndRoutes();
await refreshMenuAndRoutes(router);
}
if (clearTags) {
+11 -8
View File
@@ -75,14 +75,7 @@ export const useConfigStore = defineStore(
console.warn("[configStore] getInitConfig: 响应 data 非数组", response?.data);
return;
}
list.forEach((item: ConfigTable) => {
if (item.config_value !== undefined && item.config_key) {
configData.value[item.config_key] = item;
}
});
isConfigLoaded.value = true;
_lastFetchedAt = Date.now();
applyConfigList(list);
} catch (error) {
console.warn("[configStore] 获取配置失败:", error);
} finally {
@@ -90,6 +83,16 @@ export const useConfigStore = defineStore(
}
}
function applyConfigList(list: ConfigTable[]) {
list.forEach((item: ConfigTable) => {
if (item.config_value !== undefined && item.config_key) {
configData.value[item.config_key] = item;
}
});
isConfigLoaded.value = true;
_lastFetchedAt = Date.now();
}
return {
configData,
isConfigLoaded,
@@ -46,11 +46,16 @@ export const useNoticeStore = defineStore(
/**
* 获取通知列表
* @param force 是否强制刷新
*/
async function getNotice() {
async function getNotice(force = false) {
if (!force && isNoticeLoaded.value) return;
const response = await NoticeAPI.listNoticeAvailable();
const items = Array.isArray(response.data?.data) ? response.data.data : [];
// 过滤掉已读的通知
applyNoticeItems(items);
}
function applyNoticeItems(items: NoticeTable[]) {
const readSet = new Set(readIds.value);
const filtered = items.filter(
(item: NoticeTable) => item.id !== undefined && !readSet.has(item.id as number)
+3 -9
View File
@@ -15,13 +15,7 @@ import { ElNotification } from "element-plus";
import { store, useDictStore } from "@stores";
import type { UserInfo } from "@/api/module_system/user";
import { ResultEnum } from "@/enums/api/result.enum";
/** 延迟加载 guards 工具函数,避免 user.store 与 guards 的循环依赖 */
let _routerUtilsPromise: Promise<typeof import("@/router/guards")> | null = null;
const getRouterUtils = () => {
if (!_routerUtilsPromise) _routerUtilsPromise = import("@/router/guards");
return _routerUtilsPromise;
};
import { resetRouteInitState, resetRouterState } from "@/router/refresh";
/** {@link useUserStore} 的 `logout` 可选参数 */
export interface LogoutOptions {
@@ -258,7 +252,7 @@ export const useUserStore = defineStore(
}
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
(await getRouterUtils()).resetRouteInitState();
resetRouteInitState();
Auth.setTokens(accessToken, refreshToken, rememberMe.value);
setToken(accessToken, refreshToken);
@@ -299,7 +293,7 @@ export const useUserStore = defineStore(
resetAllState();
sessionStorage.removeItem("iframeRoutes");
useMenuStore().setHomePath("");
(await getRouterUtils()).resetRouterState(500);
resetRouterState(500);
if (shouldNavigate) {
const currentRoute = router.currentRoute.value;
+3 -8
View File
@@ -1,14 +1,9 @@
import type {
AuthDirective,
RolesDirective,
RippleDirective,
HighlightDirective,
} from "@/directives";
import type { Directive } from "vue";
import type { HighlightDirective, RippleDirective } from "@/directives";
declare module "vue" {
export interface GlobalDirectives {
vAuth: AuthDirective;
vRoles: RolesDirective;
vHasPerm: Directive;
vRipple: RippleDirective;
vHighlight: HighlightDirective;
}
@@ -187,6 +187,12 @@ import { getDashboardMock } from "@/mock/dashboard";
import bannerIcon4 from "@imgs/3d/icon4.webp";
import cover2 from "@imgs/cover/img2.webp";
import Banner from "./modules/banner.vue";
import NewUser from "./modules/new-user.vue";
import TodoList from "./modules/todo-list.vue";
import CardList from "./modules/card-list.vue";
import AboutProject from "./modules/about-project.vue";
import QuickLinks from "./modules/quick-links.vue";
const mock = getDashboardMock();
const loading = ref(false);
@@ -220,13 +226,6 @@ const FaImageCard = defineAsyncComponent(
const FaTimelineListCard = defineAsyncComponent(
() => import("@/components/cards/fa-timeline-list-card/index.vue")
);
const Banner = defineAsyncComponent(() => import("./modules/banner.vue"));
const NewUser = defineAsyncComponent(() => import("./modules/new-user.vue"));
const TodoList = defineAsyncComponent(() => import("./modules/todo-list.vue"));
const CardList = defineAsyncComponent(() => import("./modules/card-list.vue"));
const AboutProject = defineAsyncComponent(() => import("./modules/about-project.vue"));
const QuickLinks = defineAsyncComponent(() => import("./modules/quick-links.vue"));
function handleBannerDemoConfirm() {
// TODO: 接入真实操作
}
@@ -324,12 +324,14 @@ const {
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
@@ -92,7 +92,7 @@
label-suffix=":"
:label-width="100"
label-position="right"
:span="24"
:span="12"
:gutter="16"
:show-reset="false"
:show-submit="false"
@@ -299,8 +299,20 @@ const {
{ prop: "datetime_val", label: "日期时间", minWidth: 168, showOverflowTooltip: true },
{ prop: "text_val", label: "长文本", minWidth: 120, showOverflowTooltip: true },
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "created_by",
label: "创建人",
@@ -61,11 +61,7 @@
label="数据库名称"
:show-overflow-tooltip="true"
></ElTableColumn>
<ElTableColumn
prop="table_name"
label="表名称"
:show-overflow-tooltip="true"
></ElTableColumn>
<ElTableColumn prop="table_name" label="表名称" :show-overflow-tooltip="true"></ElTableColumn>
<ElTableColumn
prop="table_comment"
label="表描述"
@@ -705,6 +705,10 @@ async function handleDelete(row?: GenTableSchema): Promise<void> {
type GencodeSearchForm = {
table_name?: string;
table_comment?: string;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function buildGencodeRowActions(row: GenTableSchema): TableOperationAction[] {
@@ -748,6 +752,10 @@ function formatGencodeOperationCell(row: GenTableSchema) {
const searchForm = ref<GencodeSearchForm>({
table_name: undefined,
table_comment: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -819,12 +827,14 @@ const useTableResult = useTable({
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
@@ -865,6 +875,16 @@ async function handleSearchBarSearch(params: GencodeSearchForm) {
replaceSearchParams({
table_name: params.table_name,
table_comment: params.table_comment,
created_id: params.created_id,
updated_id: params.updated_id,
created_time:
Array.isArray(params.created_time) && params.created_time.length === 2
? params.created_time
: undefined,
updated_time:
Array.isArray(params.updated_time) && params.updated_time.length === 2
? params.updated_time
: undefined,
});
getData();
}
@@ -873,6 +893,10 @@ async function onResetSearch() {
searchForm.value = {
table_name: undefined,
table_comment: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
+4 -1
View File
@@ -372,7 +372,10 @@ async function handleCacheValue(cacheKey: string) {
const data = response.data.data;
cacheForm.value = {
...data,
cache_value: typeof data.cache_value === "string" ? data.cache_value : JSON.stringify(data.cache_value, null, 2),
cache_value:
typeof data.cache_value === "string"
? data.cache_value
: JSON.stringify(data.cache_value, null, 2),
};
} catch (error: unknown) {
if (import.meta.env.DEV) console.error("获取缓存内容失败:", error);
@@ -21,6 +21,18 @@
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="name">
<ElInput
class="custom-height"
v-model.trim="registerForm.name"
clearable
:placeholder="$t('register.placeholder.name')"
>
<template #prefix>
<ElIcon><User /></ElIcon>
</template>
</ElInput>
</ElFormItem>
<ElTooltip :visible="isCapsLock" :content="$t('login.capsLock')" placement="right">
<ElFormItem prop="password">
<ElInput
@@ -57,18 +69,6 @@
</ElInput>
</ElFormItem>
</ElTooltip>
<ElFormItem v-if="showEmail" prop="email">
<ElInput
class="custom-height"
v-model.trim="registerForm.email"
clearable
:placeholder="$t('login.placeholder.email')"
>
<template #prefix>
<ElIcon><Message /></ElIcon>
</template>
</ElInput>
</ElFormItem>
<ElFormItem>
<div class="flex flex-wrap items-center gap-2">
<ElCheckbox v-model="registerAgreementReadModel">
@@ -108,7 +108,7 @@
</template>
<script setup lang="ts">
import { Lock, Message, User } from "@element-plus/icons-vue";
import { Lock, User } from "@element-plus/icons-vue";
import type { RegisterForm } from "@/api/module_system/user";
import type { FormRules } from "element-plus";
@@ -117,14 +117,13 @@ const registerForm = defineModel<RegisterForm>("registerForm", { required: true
defineOptions({ name: "FaLoginRegisterPanel" });
interface Props {
registerRules: FormRules<RegisterForm & { email: string }>;
registerRules: FormRules<RegisterForm>;
formKey: number | string;
registerLoading: boolean;
userAgreementHref: string;
showEmail?: boolean;
}
withDefaults(defineProps<Props>(), { showEmail: false });
withDefaults(defineProps<Props>(), {});
const registerAgreementReadModel = defineModel<boolean>("registerAgreementRead", {
required: true,
@@ -90,7 +90,6 @@
:register-rules="registerRules"
:form-key="formKey"
:register-loading="registerLoading"
:show-email="true"
:user-agreement-href="userAgreementHref"
@submit="submitRegister"
@to-login="setAuthPanel('login')"
@@ -373,11 +372,11 @@ const codeLoading = ref(false);
const registerAgreementRead = ref(false);
const registerForm = reactive<RegisterForm & { email: string }>({
const registerForm = reactive<RegisterForm>({
username: "",
name: "",
password: "",
confirmPassword: "",
email: "",
});
const forgetForm = reactive<ForgetPasswordForm>({
@@ -409,8 +408,9 @@ const validateRegisterConfirm = (_rule: unknown, value: string, callback: (e?: E
callback();
};
const registerRules = computed<FormRules<RegisterForm & { email: string }>>(() => ({
const registerRules = computed<FormRules<RegisterForm>>(() => ({
username: [{ required: true, message: t("login.message.username.required"), trigger: "blur" }],
name: [{ required: true, message: "请输入昵称", trigger: "blur" }],
password: [
{ required: true, validator: validateRegisterPassword, trigger: "blur" },
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
@@ -420,14 +420,6 @@ const registerRules = computed<FormRules<RegisterForm & { email: string }>>(() =
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
{ validator: validateRegisterConfirm, trigger: "blur" },
],
email: [
{ required: true, message: t("login.email.required"), trigger: "blur" },
{
type: "email",
message: t("login.email.invalid"),
trigger: "blur",
},
],
}));
const validateForgetConfirm = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
@@ -662,7 +654,7 @@ async function submitRegister() {
registerForm.username = "";
registerForm.password = "";
registerForm.confirmPassword = "";
registerForm.email = "";
registerForm.name = "";
registerAgreementRead.value = false;
setAuthPanel("login");
await handleSubmit();
@@ -415,8 +415,20 @@ const { columnChecks, columns } = useTableColumns<DeptTable>(
},
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -225,12 +225,14 @@ type OpSearchForm = {
request_path?: string;
request_ip?: string;
created_time?: string[];
updated_time?: string[];
};
const opSearchForm = ref<OpSearchForm>({
request_path: undefined,
request_ip: undefined,
created_time: undefined,
updated_time: undefined,
});
const opShowSearchBar = ref(true);
const opSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
@@ -261,6 +263,8 @@ function buildOpReplaceParams(p: OpSearchForm): Record<string, unknown> {
request_ip: p.request_ip,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -323,7 +327,13 @@ const {
},
{ prop: "process_time", label: "处理时间", minWidth: 120 },
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -385,6 +395,7 @@ function onOpResetSearch() {
request_path: undefined,
request_ip: undefined,
created_time: undefined,
updated_time: undefined,
};
void opResetSearchParams();
}
@@ -464,12 +475,18 @@ async function handleOpBatchDelete() {
// ==================== ====================
type LoginSearchForm = { username?: string; status?: number; created_time?: string[] };
type LoginSearchForm = {
username?: string;
status?: number;
created_time?: string[];
updated_time?: string[];
};
const loginSearchForm = ref<LoginSearchForm>({
username: undefined,
status: undefined,
created_time: undefined,
updated_time: undefined,
});
const loginShowSearchBar = ref(true);
const loginSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
@@ -522,6 +539,8 @@ function buildLoginReplaceParams(p: LoginSearchForm): Record<string, unknown> {
: undefined,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -586,7 +605,13 @@ const {
{ prop: "request_os", label: "操作系统", minWidth: 120 },
{ prop: "request_browser", label: "浏览器", minWidth: 180, showOverflowTooltip: true },
{ prop: "msg", label: "提示消息", minWidth: 200, showOverflowTooltip: true },
{ prop: "created_time", label: "登录时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "登录时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -621,7 +646,12 @@ async function handleLoginSearch(params: LoginSearchForm) {
}
function onLoginResetSearch() {
loginSearchForm.value = { username: undefined, status: undefined, created_time: undefined };
loginSearchForm.value = {
username: undefined,
status: undefined,
created_time: undefined,
updated_time: undefined,
};
void loginResetSearchParams();
}
@@ -374,6 +374,7 @@ type MenuSearchForm = {
name?: string;
status?: number;
created_time?: string[];
updated_time?: string[];
};
function buildMenuListQuery(p: MenuSearchForm): MenuPageQuery {
@@ -382,6 +383,8 @@ function buildMenuListQuery(p: MenuSearchForm): MenuPageQuery {
status: p.status,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -440,6 +443,7 @@ const searchForm = ref<MenuSearchForm>({
name: undefined,
status: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
@@ -804,6 +808,7 @@ async function onResetSearch() {
name: undefined,
status: undefined,
created_time: undefined,
updated_time: undefined,
};
await loadMenuData();
}
@@ -1045,6 +1050,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
visible: false,
},
@@ -1052,6 +1058,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
visible: false,
},
@@ -158,8 +158,10 @@ type NoticeSearchForm = {
notice_title?: string;
notice_type?: string;
status?: number;
created_time?: string[];
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function noticeTypeLabel(val?: string) {
@@ -173,8 +175,10 @@ const searchForm = ref<NoticeSearchForm>({
notice_title: undefined,
notice_type: undefined,
status: undefined,
created_time: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -306,7 +310,7 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
await refreshUpdate();
},
onSubmitSuccess: async () => {
await noticeStore.getNotice();
await noticeStore.getNotice(true);
},
});
@@ -410,8 +414,20 @@ const {
},
},
{ prop: "description", label: "描述", minWidth: 140, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "created_id",
label: "创建人",
@@ -442,8 +458,11 @@ function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown>
notice_type: p.notice_type,
status: p.status,
created_id: p.created_id,
updated_id: p.updated_id,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -469,8 +488,10 @@ async function onResetSearch() {
notice_title: undefined,
notice_type: undefined,
status: undefined,
created_time: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
@@ -479,7 +500,7 @@ async function deleteNoticeRow(id: number, name: string) {
try {
await confirmDelete(`确定删除「${name}」吗?`);
await NoticeAPI.deleteNotice([id]);
await noticeStore.getNotice();
await noticeStore.getNotice(true);
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
@@ -540,7 +561,7 @@ async function handleBatchDelete() {
);
batchDeleting.value = true;
await NoticeAPI.deleteNotice(ids);
await noticeStore.getNotice();
await noticeStore.getNotice(true);
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
@@ -562,7 +583,7 @@ async function handleMoreClick(value: "enable" | "disable") {
const status = value === "enable" ? 0 : 1;
await NoticeAPI.batchNotice({ ids, status });
await refreshData();
await noticeStore.getNotice();
await noticeStore.getNotice(true);
} catch {
//
} finally {
@@ -440,15 +440,15 @@ onMounted(async () => {
<style scoped>
:deep(.el-card) {
flex: 1;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
:deep(.el-card__body) {
flex: 1;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
@@ -460,8 +460,8 @@ onMounted(async () => {
}
:deep(.el-tabs__content) {
overflow-y: auto;
flex: 1;
min-height: 0;
overflow-y: auto;
}
</style>
@@ -147,8 +147,10 @@ const userStore = useUserStore();
type PositionSearchForm = {
name?: string;
status?: number;
created_time?: string[];
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function normalizePositionQuery(params: Record<string, unknown>): PositionPageQuery {
@@ -160,8 +162,11 @@ function buildPositionReplaceParams(p: PositionSearchForm): Record<string, unkno
name: p.name,
status: p.status,
created_id: p.created_id,
updated_id: p.updated_id,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -222,8 +227,10 @@ function formatPositionOperationCell(
const searchForm = ref<PositionSearchForm>({
name: undefined,
status: undefined,
created_time: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -308,8 +315,20 @@ const {
},
{ prop: "order", label: "岗位排序", width: 100, showOverflowTooltip: true },
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "created_id",
label: "创建人",
@@ -488,8 +507,10 @@ async function onResetSearch() {
searchForm.value = {
name: undefined,
status: undefined,
created_time: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
@@ -186,7 +186,10 @@ defineOptions({
type RoleSearchForm = {
name?: string;
status?: number;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function normalizeRoleQuery(params: Record<string, unknown>): TablePageQuery {
@@ -198,8 +201,12 @@ function buildRoleReplaceParams(p: RoleSearchForm): Record<string, unknown> {
return {
name: p.name,
status: p.status,
created_id: p.created_id,
updated_id: p.updated_id,
created_time:
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
updated_time:
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
};
}
@@ -303,7 +310,10 @@ function formatRoleOperationCell(row: RoleTable, ctx: Parameters<typeof buildRol
const searchForm = ref<RoleSearchForm>({
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -576,8 +586,20 @@ const {
},
},
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -620,7 +642,10 @@ async function onResetSearch() {
searchForm.value = {
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
@@ -62,7 +62,12 @@
<FaSvgIcon :icon="typeIcon(item.ticket_type!)" />
</span>
<span class="flex-1 truncate text-sm font-semibold">{{ item.title }}</span>
<ElTag size="small" :type="statusTagType(String(item.status))" effect="dark" class="shrink-0">
<ElTag
size="small"
:type="statusTagType(String(item.status))"
effect="dark"
class="shrink-0"
>
{{ statusLabel(item.status ?? 0) }}
</ElTag>
</div>
@@ -70,12 +75,21 @@
<template #default="{ item }">
<div class="flex flex-col">
<div class="flex items-center gap-1.5 text-xs" style="color: var(--el-text-color-secondary)">
<div
class="flex items-center gap-1.5 text-xs"
style="color: var(--el-text-color-secondary)"
>
<FaSvgIcon icon="ri:user-3-line" class="shrink-0" />
<span>{{ item.created_by?.name ?? "—" }} · {{ item.created_time?.slice(0, 10) ?? "" }}</span>
<span
>{{ item.created_by?.name ?? "—" }} ·
{{ item.created_time?.slice(0, 10) ?? "" }}</span
>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 text-xs" style="color: var(--el-text-color-secondary)">
<div
class="flex items-center gap-1.5 text-xs"
style="color: var(--el-text-color-secondary)"
>
<FaSvgIcon icon="ri:user-add-line" class="shrink-0" />
<span>{{ item.assigned_by?.name ?? "未分配" }}</span>
</div>
@@ -88,7 +102,9 @@
<template #footer="{ item }">
<div class="flex items-center gap-1">
<ElButton size="small" link type="primary" @click="handleOpenDialog('detail', item.id!)">详情</ElButton>
<ElButton size="small" link type="primary" @click="handleOpenDialog('detail', item.id!)"
>详情</ElButton
>
<ElButton
v-if="item.status! < 3"
v-hasPerm="['module_system:ticket:update']"
@@ -96,7 +112,8 @@
link
type="primary"
@click="handleOpenDialog('update', item.id!)"
>处理</ElButton>
>处理</ElButton
>
<ElDropdown v-if="showCardMore(item)" trigger="click">
<ElButton size="small" link type="primary" class="px-1 py-0.5 text-base">
<ElIcon><MoreFilled /></ElIcon>
@@ -394,6 +411,7 @@ type TicketSearchForm = {
ticket_type?: string;
status?: number;
created_id?: number;
updated_id?: number;
assigned_id?: number;
};
@@ -402,6 +420,7 @@ const searchForm = ref<TicketSearchForm>({
ticket_type: "",
status: undefined,
created_id: undefined,
updated_id: undefined,
assigned_id: undefined,
});
const showSearchBar = ref(true);
@@ -513,6 +532,7 @@ async function handleSearchBarSearch(params: Record<string, unknown>) {
ticket_type: (params.ticket_type as string) ?? "",
status: params.status !== undefined ? Number(params.status) : undefined,
created_id: params.created_id as number | undefined,
updated_id: params.updated_id as number | undefined,
assigned_id: params.assigned_id as number | undefined,
};
pageNo.value = 1;
@@ -525,6 +545,7 @@ async function onResetSearch() {
ticket_type: "",
status: undefined,
created_id: undefined,
updated_id: undefined,
assigned_id: undefined,
};
pageNo.value = 1;
@@ -109,21 +109,13 @@
<FaStatusTag v-else-if="row?.gender === '1'" type="warning" label="女" />
<FaStatusTag v-else type="info" label="未知" />
</template>
<!-- 角色 数组 join 渲染 -->
<!-- 角色 根据 IDs 从选项解析名称 -->
<template #roles="{ row }">
{{
(row as unknown as UserInfo)?.roles
? (row as unknown as UserInfo).roles!.map((item) => item.name).join("、")
: ""
}}
{{ resolveLabels((row as UserInfo).role_ids, roleOptions) }}
</template>
<!-- 岗位 数组 join 渲染 -->
<!-- 岗位 根据 IDs 从选项解析名称 -->
<template #positions="{ row }">
{{
(row as unknown as UserInfo)?.positions
? (row as unknown as UserInfo).positions!.map((item) => item.name).join("、")
: ""
}}
{{ resolveLabels((row as UserInfo).position_ids, positionOptions) }}
</template>
</FaDescriptions>
</template>
@@ -249,7 +241,9 @@ type UserSearchForm = {
name?: string;
status?: number;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
@@ -258,8 +252,11 @@ function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
name: u.name,
status: u.status,
created_id: u.created_id,
updated_id: u.updated_id,
created_time:
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
updated_time:
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
};
}
@@ -354,6 +351,23 @@ const positionOptions = ref<Array<{ value: number; label: string; disabled?: boo
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
const detailFormData = ref<UserInfo>({});
interface OptionItem {
value: number;
label: string;
disabled?: boolean;
}
function resolveLabels(
ids: (number | undefined)[] | undefined,
options: OptionItem[] | undefined
): string {
if (!ids || !Array.isArray(ids) || !options) return "";
return ids
.filter((id): id is number => id !== undefined && id !== null)
.map((id) => options.find((o) => o.value === id)?.label ?? String(id))
.join("、");
}
// +
const userDetailItems: DescriptionsItem[] = [
{ label: "编号", prop: "id" },
@@ -361,7 +375,7 @@ const userDetailItems: DescriptionsItem[] = [
{ label: "账号", prop: "username" },
{ label: "用户名", prop: "name" },
{ label: "性别", prop: "gender", slot: "gender" }, // Tag
{ label: "部门", prop: "dept.name" }, // a.b.c
{ label: "部门", prop: "dept_name" },
{ label: "角色", prop: "roles", slot: "roles" }, // join
{ label: "岗位", prop: "positions", slot: "positions" }, // join
{ label: "邮箱", prop: "email" },
@@ -463,7 +477,9 @@ const searchForm = ref<UserSearchForm>({
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -611,7 +627,7 @@ const {
prop: "dept",
label: "部门",
minWidth: 100,
formatter: (row: UserInfo) => row.dept?.name ?? "—",
formatter: (row: UserInfo) => row.dept_name ?? "—",
},
{
prop: "gender",
@@ -622,8 +638,20 @@ const {
"1": { type: "warning", text: "女" },
},
},
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -771,7 +799,9 @@ async function onResetSearch() {
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
deptFilterId.value = undefined;
await resetSearchParams();
@@ -828,10 +858,8 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
} else if (type === "update") {
dialogVisible.title = "修改用户";
Object.assign(formData.value, response.data.data);
formData.value.role_ids = (response.data.data.roles || []).map((item) => item.id as number);
formData.value.position_ids = (response.data.data.positions || []).map(
(item) => item.id as number
);
formData.value.role_ids = (response.data.data.role_ids ?? []) as number[];
formData.value.position_ids = (response.data.data.position_ids ?? []) as number[];
}
} else {
dialogVisible.title = "新增用户";
@@ -251,8 +251,20 @@ const {
},
{ prop: "date", label: "发布日期", width: 120, showOverflowTooltip: true },
{ prop: "sort", label: "排序", width: 80 },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
@@ -528,18 +528,26 @@ onMounted(() => {
type LogSearchForm = {
status?: number;
trigger_type?: string;
created_time?: string[];
updated_time?: string[];
};
function buildLogReplaceParams(u: LogSearchForm): Record<string, unknown> {
return {
status: u.status,
trigger_type: u.trigger_type,
created_time:
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
updated_time:
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
};
}
const logSearchForm = ref<LogSearchForm>({
status: undefined,
trigger_type: undefined,
created_time: undefined,
updated_time: undefined,
});
const logSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
@@ -716,12 +724,14 @@ const {
prop: "created_time",
label: "创建时间",
minWidth: 160,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
minWidth: 160,
sortable: true,
showOverflowTooltip: true,
},
{
@@ -796,6 +806,8 @@ async function onLogResetSearch() {
logSearchForm.value = {
status: undefined,
trigger_type: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetLogSearchParams();
if (currentLogJobId.value) {
@@ -220,7 +220,12 @@
readonly
@click="openCron = true"
/>
<FaDialog v-model="openCron" title="Cron 表达式" width="min(700px, calc(100vw - 48px))" append-to-body>
<FaDialog
v-model="openCron"
title="Cron 表达式"
width="min(700px, calc(100vw - 48px))"
append-to-body
>
<FaCron v-model="cronTempValue" />
<template #footer>
<ElButton @click="openCron = false">取消</ElButton>
@@ -302,18 +307,32 @@ const dictStore = useDictStore();
type NodeSearchForm = {
name?: string;
code?: string;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function buildNodeReplaceParams(u: NodeSearchForm): Record<string, unknown> {
return {
name: u.name,
code: u.code,
created_id: u.created_id,
updated_id: u.updated_id,
created_time:
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
updated_time:
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
};
}
const searchForm = ref<NodeSearchForm>({
name: undefined,
code: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -512,6 +531,10 @@ async function onResetSearch() {
searchForm.value = {
name: undefined,
code: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
@@ -81,6 +81,10 @@ type WorkflowSearchForm = {
name?: string;
code?: string;
status?: number;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unknown> {
@@ -88,6 +92,12 @@ function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unkno
name: u.name,
code: u.code,
status: u.status,
created_id: u.created_id,
updated_id: u.updated_id,
created_time:
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
updated_time:
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
};
}
@@ -95,6 +105,10 @@ const searchForm = ref<WorkflowSearchForm>({
name: undefined,
code: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -284,6 +298,7 @@ const {
prop: "created_time",
label: "创建时间",
minWidth: 180,
sortable: true,
showOverflowTooltip: true,
},
{
@@ -309,6 +324,10 @@ async function onResetSearch() {
name: undefined,
code: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}
@@ -190,6 +190,10 @@ type NodeTypeSearchForm = {
name?: string;
code?: string;
category?: string;
created_id?: number;
updated_id?: number;
created_time?: string[];
updated_time?: string[];
};
function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unknown> {
@@ -197,6 +201,12 @@ function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unkno
name: u.name,
code: u.code,
category: u.category,
created_id: u.created_id,
updated_id: u.updated_id,
created_time:
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
updated_time:
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
};
}
@@ -204,6 +214,10 @@ const searchForm = ref<NodeTypeSearchForm>({
name: undefined,
code: undefined,
category: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
});
const showSearchBar = ref(true);
@@ -419,6 +433,7 @@ const {
prop: "created_time",
label: "创建时间",
minWidth: 170,
sortable: true,
showOverflowTooltip: true,
},
{
@@ -444,6 +459,10 @@ async function onResetSearch() {
name: undefined,
code: undefined,
category: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined,
updated_time: undefined,
};
await resetSearchParams();
}