mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
本次提交包含多项优化: 1. 移除大量冗余的文件头注释与过时的from __future__导入 2. 将CRUD的list方法统一重命名为get_list保持接口一致 3. 修复前后端状态字段类型不匹配问题,将string类型status改为number 4. 修正前端文案错别字,将"代办事项"修正为标准写法 5. 更新sqlalchemy版本并调整依赖配置 6. 新增缓存工具类替代fastapi-cache2,重构缓存调用逻辑 7. 新增开源授权函生成相关工具与数据库字段支持 8. 为多个业务模块添加防重复提交loading状态 9. 修复邮件模型的外键关联缺失问题 10. 优化pdf生成工具的导入时机与文档注释
746 lines
20 KiB
Vue
746 lines
20 KiB
Vue
<!-- 公告通知:Fa 布局 + useTable,与 dict 页一致 -->
|
||
<template>
|
||
<div class="fa-full-height">
|
||
<FaSearchBar
|
||
v-show="showSearchBar"
|
||
ref="searchBarRef"
|
||
v-model="searchForm"
|
||
:items="noticeSearchItems"
|
||
:rules="searchBarRules"
|
||
:is-expand="false"
|
||
:show-expand="true"
|
||
:show-reset="true"
|
||
:show-search="true"
|
||
:disabled-search="false"
|
||
:default-expanded="false"
|
||
@search="handleSearchBarSearch"
|
||
@reset="onResetSearch"
|
||
>
|
||
<template #created_id>
|
||
<FaUserTableSelect
|
||
:model-value="searchForm.created_id == null ? undefined : searchForm.created_id"
|
||
@update:model-value="(v: number | undefined) => (searchForm.created_id = v)"
|
||
@confirm-click="afterUserSelectSearch"
|
||
@clear-click="afterUserSelectSearch"
|
||
/>
|
||
</template>
|
||
</FaSearchBar>
|
||
|
||
<ElCard
|
||
shadow="hover"
|
||
class="fa-table-card"
|
||
:style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
|
||
>
|
||
<FaTableHeader
|
||
v-model:columns="columnChecks"
|
||
v-model:showSearchBar="showSearchBar"
|
||
:loading="loading"
|
||
@refresh="refreshData"
|
||
>
|
||
<template #left>
|
||
<FaTableHeaderLeft
|
||
:remove-ids="selectedIds"
|
||
:perm-create="['module_system:notice:create']"
|
||
:perm-export="['module_system:notice:export']"
|
||
:perm-delete="['module_system:notice:delete']"
|
||
:perm-patch="['module_system:notice:patch']"
|
||
:delete-loading="batchDeleting"
|
||
:create-loading="createLoading"
|
||
:more-loading="moreLoading"
|
||
@add="handleAdd"
|
||
@export="openExport"
|
||
@delete="handleBatchDelete"
|
||
@more="handleMoreClick"
|
||
/>
|
||
</template>
|
||
</FaTableHeader>
|
||
|
||
<FaTable
|
||
ref="faTableRef"
|
||
:loading="loading"
|
||
:data="data"
|
||
:columns="columns"
|
||
:pagination="pagination"
|
||
@selection-change="onTableSelectionChange"
|
||
@pagination:size-change="handleSizeChange"
|
||
@pagination:current-change="handleCurrentChange"
|
||
/>
|
||
</ElCard>
|
||
|
||
<FaDialog
|
||
v-model="dialogVisible.visible"
|
||
:title="dialogVisible.title"
|
||
width="920px"
|
||
dialog-class="crud-embed-dialog"
|
||
modal-class="crud-embed-dialog"
|
||
:form-mode="dialogVisible.type"
|
||
:confirm-loading="submitLoading"
|
||
@cancel="handleCloseDialog"
|
||
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
|
||
>
|
||
<template v-if="dialogVisible.type === 'detail'">
|
||
<FaDescriptions
|
||
:column="4"
|
||
:data="detailFormData"
|
||
:items="noticeDetailItems"
|
||
label-width="120px"
|
||
max-height="75vh"
|
||
>
|
||
<template #notice_type="{ row }">
|
||
<FaStatusTag
|
||
:type="row?.notice_type === '1' ? 'primary' : 'warning'"
|
||
:label="noticeTypeLabel(row?.notice_type as string)"
|
||
/>
|
||
</template>
|
||
<template #notice_content>
|
||
<ElScrollbar class="notice-html-preview" view-class="p-3">
|
||
<template v-if="detailHasRenderableContent">
|
||
<div v-html="detailContentHtml" />
|
||
</template>
|
||
<p v-else class="notice-html-empty">暂无内容</p>
|
||
</ElScrollbar>
|
||
</template>
|
||
</FaDescriptions>
|
||
</template>
|
||
<template v-else>
|
||
<FaForm
|
||
:key="noticeFormRenderKey"
|
||
scrollbar
|
||
max-height="75vh"
|
||
ref="dataFormRef"
|
||
v-model="formData"
|
||
:items="noticeDialogFormItems"
|
||
:rules="rules"
|
||
label-suffix=":"
|
||
:label-width="100"
|
||
label-position="right"
|
||
:span="24"
|
||
:gutter="16"
|
||
:show-reset="false"
|
||
:show-submit="false"
|
||
class="crud-dialog-art-form"
|
||
>
|
||
<template #status>
|
||
<ElRadioGroup v-model="formData.status">
|
||
<ElRadio :value="0">启用</ElRadio>
|
||
<ElRadio :value="1">停用</ElRadio>
|
||
</ElRadioGroup>
|
||
</template>
|
||
<template #notice_content>
|
||
<FaWangEditor
|
||
:model-value="formData.notice_content ?? ''"
|
||
height="min(38vh, 280px)"
|
||
placeholder="请输入公告内容,支持完整排版与插入..."
|
||
:exclude-keys="[]"
|
||
@update:model-value="(v: string) => (formData.notice_content = v)"
|
||
/>
|
||
</template>
|
||
</FaForm>
|
||
</template>
|
||
</FaDialog>
|
||
|
||
<FaExportDialog
|
||
v-model="exportVisible"
|
||
:content-config="noticeExportContentConfig"
|
||
:query-params="exportQueryParams"
|
||
:page-data="data"
|
||
:selection-data="selectedRows"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useTable } from "@/hooks/core/useTable";
|
||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||
import { useTableSelection } from "@/hooks/core/useTableSelection";
|
||
import { useCrudForm } from "@/hooks/core/useCrudForm";
|
||
import { confirmDelete, confirmBatchDelete, confirmToggleStatus } from "@/hooks/core/useConfirm";
|
||
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
|
||
import type { ColumnOption } from "@/types/component";
|
||
import NoticeAPI, {
|
||
type NoticeForm,
|
||
type NoticePageQuery,
|
||
type NoticeTable,
|
||
} from "@/api/module_system/notice";
|
||
import { useAuth } from "@/hooks/core/useAuth";
|
||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||
import { useDictStore, useNoticeStore } from "@stores";
|
||
import type { IObject } from "@/components/modal/types";
|
||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||
import { ElMessage, ElScrollbar } from "element-plus";
|
||
import DOMPurify from "dompurify";
|
||
|
||
defineOptions({
|
||
name: "Notice",
|
||
inheritAttrs: false,
|
||
});
|
||
|
||
const dictStore = useDictStore();
|
||
const noticeStore = useNoticeStore();
|
||
const { hasAuth } = useAuth();
|
||
|
||
type NoticeSearchForm = {
|
||
notice_title?: string;
|
||
notice_type?: string;
|
||
status?: number;
|
||
created_time?: string[];
|
||
created_id?: number;
|
||
};
|
||
|
||
function normalizeNoticeQuery(params: Record<string, unknown>): NoticePageQuery {
|
||
return cleanEmptyArrayParams({ ...params }) as unknown as NoticePageQuery;
|
||
}
|
||
|
||
function noticeTypeLabel(val?: string) {
|
||
if (!val) return "";
|
||
const lab = dictStore.getDictLabel("sys_notice_type", val);
|
||
if (typeof lab === "string") return lab;
|
||
return lab.dict_label ?? val;
|
||
}
|
||
|
||
const searchForm = ref<NoticeSearchForm>({
|
||
notice_title: undefined,
|
||
notice_type: undefined,
|
||
status: undefined,
|
||
created_time: undefined,
|
||
created_id: undefined,
|
||
});
|
||
|
||
const showSearchBar = ref(true);
|
||
const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||
const searchBarRules: Record<string, unknown> = {};
|
||
|
||
const statusOptions = ref([
|
||
{ label: "启用", value: 0 },
|
||
{ label: "停用", value: 1 },
|
||
]);
|
||
|
||
const noticeTypeSearchOptions = computed(() =>
|
||
dictStore.getDictArray("sys_notice_type").map((item) => ({
|
||
label: item.dict_label,
|
||
value: item.dict_value,
|
||
}))
|
||
);
|
||
|
||
const noticeSearchItems = computed<SearchFormItem[]>(() => [
|
||
{
|
||
label: "标题",
|
||
key: "notice_title",
|
||
type: "input",
|
||
placeholder: "请输入标题",
|
||
clearable: true,
|
||
span: 6,
|
||
},
|
||
{
|
||
label: "类型",
|
||
key: "notice_type",
|
||
type: "select",
|
||
props: {
|
||
placeholder: "请选择类型",
|
||
options: noticeTypeSearchOptions.value,
|
||
clearable: true,
|
||
},
|
||
span: 6,
|
||
},
|
||
{
|
||
label: "状态",
|
||
key: "status",
|
||
type: "select",
|
||
props: {
|
||
placeholder: "请选择状态",
|
||
options: statusOptions.value,
|
||
clearable: true,
|
||
},
|
||
span: 6,
|
||
},
|
||
{
|
||
label: "创建时间",
|
||
key: "created_time",
|
||
type: "datetimerange",
|
||
span: 6,
|
||
props: {
|
||
type: "datetimerange",
|
||
rangeSeparator: "至",
|
||
startPlaceholder: "开始日期",
|
||
endPlaceholder: "结束日期",
|
||
format: "YYYY-MM-DD HH:mm:ss",
|
||
valueFormat: "YYYY-MM-DD HH:mm:ss",
|
||
style: { width: "100%" },
|
||
},
|
||
},
|
||
{
|
||
label: "创建人",
|
||
key: "created_id",
|
||
type: "input",
|
||
span: 6,
|
||
},
|
||
]);
|
||
|
||
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
|
||
|
||
// ─── 表格多选 ───
|
||
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
|
||
useTableSelection<NoticeTable>();
|
||
|
||
const createLoading = ref(false);
|
||
const moreLoading = ref(false);
|
||
|
||
// ─── 对话框状态 ───
|
||
const { dialogVisible } = useCrudDialog();
|
||
|
||
const detailFormData = ref<NoticeTable>({});
|
||
|
||
const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
||
[
|
||
{ label: "标题", prop: "notice_title" },
|
||
{ label: "类型", prop: "notice_type", slot: "notice_type" },
|
||
{
|
||
label: "状态",
|
||
prop: "status",
|
||
tag: {
|
||
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
|
||
},
|
||
},
|
||
{ label: "描述", prop: "description" },
|
||
{ label: "内容", prop: "notice_content", slot: "notice_content", span: 4 },
|
||
{ label: "创建人", prop: "created_by.name" },
|
||
{ label: "更新人", prop: "updated_by.name" },
|
||
{ label: "创建时间", prop: "created_time" },
|
||
{ label: "更新时间", prop: "updated_time" },
|
||
];
|
||
|
||
/** 详情富文本 HTML(用于预览,已做 XSS 净化) */
|
||
const detailContentHtml = computed({
|
||
get: () => {
|
||
const raw = detailFormData.value.notice_content ?? "";
|
||
return DOMPurify.sanitize(raw);
|
||
},
|
||
set: (v: string) => {
|
||
detailFormData.value.notice_content = v;
|
||
},
|
||
});
|
||
|
||
/** 详情是否有可视文本 */
|
||
const detailHasRenderableContent = computed(() => {
|
||
const raw = detailFormData.value.notice_content ?? "";
|
||
if (!raw.trim()) return false;
|
||
const plain = raw
|
||
.replace(/<[^>]+>/g, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
return plain.length > 0;
|
||
});
|
||
|
||
const formData = ref<NoticeForm>({
|
||
id: undefined,
|
||
notice_title: "",
|
||
notice_type: "",
|
||
notice_content: "",
|
||
status: 0,
|
||
description: undefined,
|
||
});
|
||
|
||
const rules = reactive({
|
||
notice_title: [{ required: true, message: "请输入公告通知标题", trigger: "blur" }],
|
||
notice_type: [{ required: true, message: "请选择公告通知类型", trigger: "blur" }],
|
||
notice_content: [{ required: true, message: "请输入公告通知内容", trigger: "blur" }],
|
||
status: [{ required: true, message: "请选择公告通知状态", trigger: "blur" }],
|
||
});
|
||
|
||
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
|
||
const noticeFormRenderKey = ref(0);
|
||
|
||
const initialFormData: NoticeForm = {
|
||
id: undefined,
|
||
notice_title: "",
|
||
notice_type: "",
|
||
notice_content: "",
|
||
status: 0,
|
||
description: undefined,
|
||
};
|
||
|
||
// ─── CRUD 表单 ───
|
||
const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
|
||
useCrudForm<NoticeForm>({
|
||
formData,
|
||
initialFormData,
|
||
dialogVisible,
|
||
dataFormRef,
|
||
formRenderKey: noticeFormRenderKey,
|
||
detailApi: NoticeAPI.detailNotice,
|
||
createApi: NoticeAPI.createNotice,
|
||
updateApi: NoticeAPI.updateNotice,
|
||
titles: { create: "新增公告通知", update: "修改公告通知", detail: "公告通知详情" },
|
||
detailFormData,
|
||
onCreateSuccess: async () => {
|
||
await refreshCreate();
|
||
},
|
||
onUpdateSuccess: async () => {
|
||
await refreshUpdate();
|
||
},
|
||
onSubmitSuccess: async () => {
|
||
await noticeStore.getNotice();
|
||
},
|
||
});
|
||
|
||
async function handleAdd() {
|
||
createLoading.value = true;
|
||
try {
|
||
await handleOpenDialog("create");
|
||
} finally {
|
||
createLoading.value = false;
|
||
}
|
||
}
|
||
|
||
const noticeDialogFormItems = computed<FormItem[]>(() => [
|
||
{
|
||
label: "标题",
|
||
key: "notice_title",
|
||
type: "input",
|
||
span: 24,
|
||
props: { placeholder: "请输入标题", maxlength: 50 },
|
||
},
|
||
{
|
||
label: "描述",
|
||
key: "description",
|
||
type: "input",
|
||
span: 24,
|
||
props: {
|
||
type: "textarea",
|
||
rows: 2,
|
||
maxlength: 100,
|
||
showWordLimit: true,
|
||
placeholder: "请输入描述",
|
||
},
|
||
},
|
||
{
|
||
label: "类型",
|
||
key: "notice_type",
|
||
type: "select",
|
||
span: 24,
|
||
props: {
|
||
placeholder: "请选择类型",
|
||
clearable: true,
|
||
class: "w-full! max-w-md",
|
||
options: dictStore.getDictArray("sys_notice_type").map((item) => ({
|
||
label: item.dict_label,
|
||
value: item.dict_value,
|
||
})),
|
||
},
|
||
},
|
||
{
|
||
label: "状态",
|
||
key: "status",
|
||
type: "input",
|
||
span: 24,
|
||
placeholder: "",
|
||
},
|
||
{
|
||
label: "内容",
|
||
key: "notice_content",
|
||
type: "input",
|
||
span: 24,
|
||
placeholder: "",
|
||
},
|
||
]);
|
||
|
||
const {
|
||
columns,
|
||
columnChecks,
|
||
data,
|
||
loading,
|
||
pagination,
|
||
searchParams,
|
||
getData,
|
||
replaceSearchParams,
|
||
resetSearchParams,
|
||
handleSizeChange,
|
||
handleCurrentChange,
|
||
refreshData,
|
||
refreshCreate,
|
||
refreshUpdate,
|
||
refreshRemove,
|
||
} = useTable({
|
||
core: {
|
||
apiFn: NoticeAPI.listNotice,
|
||
apiParams: {
|
||
page_no: 1,
|
||
page_size: 10,
|
||
},
|
||
columnsFactory: resolveStatusColumns<NoticeTable>(() => [
|
||
{ type: "selection", width: 48, fixed: "left" },
|
||
{ type: "globalIndex", width: 56, label: "序号" },
|
||
{ prop: "notice_title", label: "通知标题", minWidth: 140, showOverflowTooltip: true },
|
||
{
|
||
prop: "status",
|
||
label: "状态",
|
||
width: 88,
|
||
status: {
|
||
0: { type: "success", text: "启用" },
|
||
1: { type: "danger", text: "停用" },
|
||
},
|
||
},
|
||
{
|
||
prop: "notice_type",
|
||
label: "类型",
|
||
minWidth: 100,
|
||
status: {
|
||
"1": { type: "primary", text: "通知" },
|
||
"2": { type: "warning", text: "公告" },
|
||
},
|
||
},
|
||
{ prop: "notice_content", label: "内容", minWidth: 200, showOverflowTooltip: true },
|
||
{ 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_id",
|
||
label: "创建人",
|
||
minWidth: 100,
|
||
formatter: (row: NoticeTable) => row.created_by?.name ?? "—",
|
||
},
|
||
{
|
||
prop: "updated_id",
|
||
label: "更新人",
|
||
minWidth: 100,
|
||
formatter: (row: NoticeTable) => row.updated_by?.name ?? "—",
|
||
},
|
||
{
|
||
prop: "operation",
|
||
label: "操作",
|
||
width: 220,
|
||
fixed: "right",
|
||
align: "right",
|
||
formatter: (row: NoticeTable) => formatNoticeOperationCell(row),
|
||
},
|
||
]),
|
||
},
|
||
});
|
||
|
||
const noticeCrudCols = computed(() =>
|
||
columns.value.map((c: ColumnOption<NoticeTable>) => {
|
||
const t = (c as { type?: string }).type;
|
||
return {
|
||
prop: c.prop,
|
||
label: c.label,
|
||
type: t === "selection" ? ("selection" as const) : ("default" as const),
|
||
show: true,
|
||
};
|
||
})
|
||
);
|
||
|
||
const exportQueryParams = computed(() => {
|
||
const sp = stripPaginationParams(searchParams as Record<string, unknown>);
|
||
return normalizeNoticeQuery(sp);
|
||
});
|
||
|
||
const noticeExportContentConfig = computed(() => ({
|
||
permPrefix: "module_system:notice",
|
||
cols: noticeCrudCols.value,
|
||
exportsBlobAction: async (params: IObject) => {
|
||
const merged = normalizeNoticeQuery({
|
||
...(exportQueryParams.value as unknown as Record<string, unknown>),
|
||
...params,
|
||
} as Record<string, unknown>);
|
||
const res = await NoticeAPI.exportNotice(merged as NoticePageQuery);
|
||
return res.data as Blob;
|
||
},
|
||
}));
|
||
|
||
const { exportVisible, openExport } = useImportExport();
|
||
|
||
function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown> {
|
||
return {
|
||
notice_title: p.notice_title,
|
||
notice_type: p.notice_type,
|
||
status: p.status,
|
||
created_id: p.created_id,
|
||
created_time:
|
||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||
};
|
||
}
|
||
|
||
async function handleSearchBarSearch(params: NoticeSearchForm) {
|
||
await searchBarRef.value?.validate?.();
|
||
replaceSearchParams(buildNoticeReplaceParams(params));
|
||
getData();
|
||
}
|
||
|
||
async function applyNoticeSearchFromForm() {
|
||
await searchBarRef.value?.validate?.();
|
||
replaceSearchParams(buildNoticeReplaceParams(searchForm.value));
|
||
getData();
|
||
}
|
||
|
||
async function afterUserSelectSearch() {
|
||
await nextTick();
|
||
await applyNoticeSearchFromForm();
|
||
}
|
||
|
||
function onResetSearch() {
|
||
searchForm.value = {
|
||
notice_title: undefined,
|
||
notice_type: undefined,
|
||
status: undefined,
|
||
created_time: undefined,
|
||
created_id: undefined,
|
||
};
|
||
void resetSearchParams();
|
||
}
|
||
|
||
async function deleteNoticeRow(id: number) {
|
||
try {
|
||
await confirmDelete();
|
||
await NoticeAPI.deleteNotice([id]);
|
||
await noticeStore.getNotice();
|
||
faTableRef.value?.elTableRef?.clearSelection();
|
||
await refreshRemove();
|
||
} catch {
|
||
ElMessage.info("删除取消");
|
||
}
|
||
}
|
||
|
||
function buildNoticeRowActions(row: NoticeTable): TableOperationAction[] {
|
||
const all: TableOperationAction[] = [
|
||
{
|
||
key: "detail",
|
||
label: "详情",
|
||
artType: "view",
|
||
perm: "module_system:notice:detail",
|
||
run: () => {
|
||
if (row.id != null) void handleOpenDialog("detail", row.id);
|
||
},
|
||
},
|
||
{
|
||
key: "edit",
|
||
label: "编辑",
|
||
artType: "edit",
|
||
icon: "ri:edit-2-line",
|
||
perm: "module_system:notice:update",
|
||
run: () => {
|
||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||
},
|
||
},
|
||
{
|
||
key: "delete",
|
||
label: "删除",
|
||
artType: "delete",
|
||
icon: "ri:delete-bin-4-line",
|
||
perm: "module_system:notice:delete",
|
||
run: () => {
|
||
if (row.id != null) deleteNoticeRow(row.id);
|
||
},
|
||
},
|
||
];
|
||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||
}
|
||
|
||
function formatNoticeOperationCell(row: NoticeTable) {
|
||
return renderTableOperationCell(buildNoticeRowActions(row), {
|
||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 notice-table-actions",
|
||
});
|
||
}
|
||
|
||
async function handleBatchDelete() {
|
||
const ids = selectedIds.value;
|
||
if (ids.length === 0) return;
|
||
try {
|
||
await confirmBatchDelete(ids.length);
|
||
batchDeleting.value = true;
|
||
await NoticeAPI.deleteNotice(ids);
|
||
await noticeStore.getNotice();
|
||
faTableRef.value?.elTableRef?.clearSelection();
|
||
await refreshRemove();
|
||
} catch {
|
||
ElMessage.info("删除取消");
|
||
} finally {
|
||
batchDeleting.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleMoreClick(status: number) {
|
||
const ids = selectedIds.value;
|
||
if (!ids.length) {
|
||
ElMessage.warning("请先选择要操作的数据");
|
||
return;
|
||
}
|
||
try {
|
||
await confirmToggleStatus(status);
|
||
moreLoading.value = true;
|
||
await NoticeAPI.batchNotice({ ids, status });
|
||
await refreshData();
|
||
await noticeStore.getNotice();
|
||
} catch {
|
||
// 用户取消或操作失败
|
||
} finally {
|
||
moreLoading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await dictStore.getDict(["sys_notice_type"]);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* 富文本预览区域阅读样式(与 FaWangEditor 输出 HTML 展示一致) */
|
||
.notice-html-preview {
|
||
box-sizing: border-box;
|
||
min-height: 120px;
|
||
max-height: min(360px, 45vh);
|
||
background-color: var(--el-bg-color);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: calc(var(--custom-radius) / 3 + 2px);
|
||
}
|
||
|
||
.notice-html-empty {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.notice-html-preview :deep(h1),
|
||
.notice-html-preview :deep(h2),
|
||
.notice-html-preview :deep(h3) {
|
||
margin: 12px 0 8px;
|
||
}
|
||
|
||
.notice-html-preview :deep(p) {
|
||
margin: 8px 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.notice-html-preview :deep(table) {
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.notice-html-preview :deep(table th),
|
||
.notice-html-preview :deep(table td) {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.notice-html-preview :deep(pre) {
|
||
padding: 12px;
|
||
margin: 12px 0;
|
||
overflow-x: auto;
|
||
background-color: var(--el-fill-color-light);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.notice-html-preview :deep(blockquote) {
|
||
padding-left: 16px;
|
||
margin: 12px 0;
|
||
color: var(--el-text-color-regular);
|
||
border-left: 4px solid var(--el-color-primary);
|
||
}
|
||
|
||
.notice-html-preview :deep(img) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
</style>
|