mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 13:05:18 +00:00
chore: 整理项目配置与代码优化
1. 统一前端环境变量命名,新增通用项目标题配置 2. 修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析 3. 优化表格列排序、搜索过滤功能与页面布局样式 4. 重构路由与状态管理代码,移除循环依赖 5. 更新依赖版本与脚本命令,修复构建警告 6. 调整多语言文案与快捷入口配置 7. 修复通知中心样式与接口返回格式
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -369,7 +369,7 @@
|
||||
"user": "普通用户"
|
||||
},
|
||||
"placeholder": {
|
||||
"username": "请输入用户名",
|
||||
"username": "请输入账号",
|
||||
"password": "请输入密码",
|
||||
"email": "请输入邮箱",
|
||||
"slider": "请拖动滑块完成验证"
|
||||
@@ -473,7 +473,7 @@
|
||||
"username": "请输入账号",
|
||||
"password": "请输入密码",
|
||||
"confirmPassword": "请再次输入密码",
|
||||
"email": "请输入邮箱"
|
||||
"name": "请输入昵称"
|
||||
},
|
||||
"rule": {
|
||||
"confirmPasswordRequired": "请再次输入密码",
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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,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";
|
||||
/** 菜单处理(获取、过滤、壳层补全) */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "创建人",
|
||||
|
||||
+1
-5
@@ -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
@@ -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);
|
||||
|
||||
+15
-16
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user