feat(finance): add financial disclosure publishing
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@lehuo/contracts": "workspace:*",
|
||||
"dompurify": "^3.4.13",
|
||||
"mammoth": "^1.12.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.20",
|
||||
"vue-router": "^4.5.1"
|
||||
|
||||
@@ -17,6 +17,7 @@ import NotificationDeliveriesView from './views/NotificationDeliveriesView.vue';
|
||||
import StatisticsView from './views/StatisticsView.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
import AdminIcon from './components/AdminIcon.vue';
|
||||
import FinancialDisclosuresView from './views/FinancialDisclosuresView.vue';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const form = reactive({ username: '', password: '' });
|
||||
@@ -36,6 +37,7 @@ const activeView = ref<
|
||||
| 'content'
|
||||
| 'notifications'
|
||||
| 'statistics'
|
||||
| 'financial-disclosures'
|
||||
>('dashboard');
|
||||
|
||||
onMounted(() => auth.restore());
|
||||
@@ -171,6 +173,14 @@ async function logout(): Promise<void> {
|
||||
>
|
||||
<AdminIcon name="content" />内容发布
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'financial-disclosures' }]"
|
||||
type="button"
|
||||
:aria-current="activeView === 'financial-disclosures' ? 'page' : undefined"
|
||||
@click="activeView = 'financial-disclosures'"
|
||||
>
|
||||
<AdminIcon name="content" />财务公开
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'notifications' }]"
|
||||
type="button"
|
||||
@@ -253,6 +263,7 @@ async function logout(): Promise<void> {
|
||||
<ProjectsView v-else-if="activeView === 'projects'" />
|
||||
<ActivitiesView v-else-if="activeView === 'activities'" />
|
||||
<ContentView v-else-if="activeView === 'content'" />
|
||||
<FinancialDisclosuresView v-else-if="activeView === 'financial-disclosures'" />
|
||||
<NotificationDeliveriesView v-else-if="activeView === 'notifications'" />
|
||||
<StatisticsView v-else />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type {
|
||||
CreateFinancialDisclosureRequest,
|
||||
FinancialDisclosureDetailResponse,
|
||||
PaginatedFinancialDisclosuresResponse,
|
||||
TransitionFinancialDisclosureRequest,
|
||||
UpdateFinancialDisclosureRequest,
|
||||
} from '@lehuo/contracts';
|
||||
import { apiRequest } from './api-client';
|
||||
const query = (value: Record<string, string | number | undefined>) => {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(value).forEach(
|
||||
([key, item]) => item !== undefined && params.set(key, String(item)),
|
||||
);
|
||||
return params.toString();
|
||||
};
|
||||
export const fetchFinancialDisclosures = (value: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
status?: string;
|
||||
}) =>
|
||||
apiRequest<PaginatedFinancialDisclosuresResponse>(
|
||||
`/api/v1/admin/financial-disclosures?${query(value)}`,
|
||||
);
|
||||
export const fetchFinancialDisclosure = (id: string) =>
|
||||
apiRequest<FinancialDisclosureDetailResponse>(`/api/v1/admin/financial-disclosures/${id}`);
|
||||
export const createFinancialDisclosure = (input: CreateFinancialDisclosureRequest) =>
|
||||
apiRequest<FinancialDisclosureDetailResponse>('/api/v1/admin/financial-disclosures', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
export const updateFinancialDisclosure = (id: string, input: UpdateFinancialDisclosureRequest) =>
|
||||
apiRequest<FinancialDisclosureDetailResponse>(`/api/v1/admin/financial-disclosures/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
export const transitionFinancialDisclosure = (
|
||||
id: string,
|
||||
input: TransitionFinancialDisclosureRequest,
|
||||
) =>
|
||||
apiRequest<FinancialDisclosureDetailResponse>(
|
||||
`/api/v1/admin/financial-disclosures/${id}/transition`,
|
||||
{ method: 'POST', body: JSON.stringify(input) },
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { defaultFinancialSummary, htmlToPlainText } from './financial-document';
|
||||
describe('financial document helpers', () => {
|
||||
it('creates a short readable summary from imported HTML', () => {
|
||||
expect(htmlToPlainText('<h1>年度报告</h1><p>公益项目执行情况</p>')).toBe(
|
||||
'年度报告公益项目执行情况',
|
||||
);
|
||||
expect(defaultFinancialSummary(`<p>${'财'.repeat(130)}</p>`)).toHaveLength(120);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export function htmlToPlainText(html: string): string {
|
||||
return html
|
||||
.replace(/<br\s*\/?\s*>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ | /gi, ' ')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'|'/gi, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function defaultFinancialSummary(html: string): string {
|
||||
return htmlToPlainText(html).slice(0, 120);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
declare module 'mammoth/mammoth.browser' {
|
||||
export interface ConversionMessage {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
}
|
||||
export interface ConversionResult {
|
||||
value: string;
|
||||
messages: ConversionMessage[];
|
||||
}
|
||||
export function convertToHtml(input: { arrayBuffer: ArrayBuffer }): Promise<ConversionResult>;
|
||||
}
|
||||
@@ -27,6 +27,157 @@
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.financial-workspace {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
.financial-editor-panel {
|
||||
padding: 24px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.financial-editor-head,
|
||||
.rich-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.financial-editor-head {
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.import-button {
|
||||
display: inline-flex;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 8px;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.import-button input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
.financial-fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 140px;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.financial-fields label:first-child,
|
||||
.financial-fields label:nth-child(2) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.inline-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.inline-check input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.rich-toolbar {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.rich-toolbar button {
|
||||
min-height: 34px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #d8dee8;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #344054;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rich-editor {
|
||||
min-height: 480px;
|
||||
padding: 36px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0 0 10px 10px;
|
||||
outline: none;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.rich-editor:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgb(240 90 40 / 12%);
|
||||
}
|
||||
.rich-editor table,
|
||||
.public-rich-text table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.rich-editor th,
|
||||
.rich-editor td,
|
||||
.public-rich-text th,
|
||||
.public-rich-text td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d9dde4;
|
||||
}
|
||||
.mobile-preview {
|
||||
width: min(420px, 100%);
|
||||
min-height: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 24px;
|
||||
border: 10px solid #25282d;
|
||||
border-radius: 34px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 48px rgb(23 31 45 / 18%);
|
||||
}
|
||||
.preview-label {
|
||||
margin: -12px 0 24px;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.preview-summary {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
background: #fff5ee;
|
||||
color: #6d4b3b;
|
||||
}
|
||||
.public-rich-text {
|
||||
overflow-wrap: anywhere;
|
||||
color: #30343b;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.public-rich-text img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.status-chip {
|
||||
margin-left: 8px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: #fff0e8;
|
||||
color: #e65322;
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.financial-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.financial-fields label {
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
.rich-editor {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import DOMPurify from 'dompurify';
|
||||
import {
|
||||
FINANCIAL_DISCLOSURE_STATUSES,
|
||||
FINANCIAL_DISCLOSURE_STATUS_LABELS,
|
||||
type FinancialDisclosureDetail,
|
||||
type FinancialDisclosureListItem,
|
||||
type FinancialDisclosureStatus,
|
||||
} from '@lehuo/contracts';
|
||||
import { ApiClientError } from '../api/api-client';
|
||||
import {
|
||||
createFinancialDisclosure,
|
||||
fetchFinancialDisclosure,
|
||||
fetchFinancialDisclosures,
|
||||
transitionFinancialDisclosure,
|
||||
updateFinancialDisclosure,
|
||||
} from '../api/financial-disclosures';
|
||||
import { defaultFinancialSummary } from '../financial-document';
|
||||
|
||||
const items = ref<FinancialDisclosureListItem[]>([]),
|
||||
loading = ref(true),
|
||||
busy = ref(false),
|
||||
message = ref(''),
|
||||
editing = ref(false),
|
||||
previewing = ref(false),
|
||||
editor = ref<HTMLElement>();
|
||||
const filter = ref<FinancialDisclosureStatus | ''>(''),
|
||||
page = ref(1),
|
||||
total = ref(0),
|
||||
pageSize = 20;
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)));
|
||||
const form = reactive({
|
||||
publicId: '',
|
||||
revision: 0,
|
||||
title: '',
|
||||
summary: '',
|
||||
richText: '',
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
});
|
||||
const errorText = (error: unknown) =>
|
||||
error instanceof ApiClientError
|
||||
? `${error.message}${error.requestId ? `(编号:${error.requestId})` : ''}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: '操作失败,请稍后重试';
|
||||
const sanitizeRichText = (html: string) =>
|
||||
DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [
|
||||
'p',
|
||||
'br',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'strong',
|
||||
'b',
|
||||
'em',
|
||||
'i',
|
||||
'u',
|
||||
's',
|
||||
'blockquote',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
'a',
|
||||
'img',
|
||||
'hr',
|
||||
'span',
|
||||
],
|
||||
ALLOWED_ATTR: ['href', 'title', 'src', 'alt', 'colspan', 'rowspan', 'style'],
|
||||
});
|
||||
async function load(target = 1) {
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const result = await fetchFinancialDisclosures({
|
||||
page: target,
|
||||
pageSize,
|
||||
...(filter.value ? { status: filter.value } : {}),
|
||||
});
|
||||
items.value = result.items;
|
||||
page.value = result.page;
|
||||
total.value = result.total;
|
||||
} catch (error) {
|
||||
message.value = errorText(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
async function newDocument() {
|
||||
Object.assign(form, {
|
||||
publicId: '',
|
||||
revision: 0,
|
||||
title: '',
|
||||
summary: '',
|
||||
richText: '<h2>财务公开</h2><p>请在此编辑公开内容。</p>',
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
});
|
||||
editing.value = true;
|
||||
previewing.value = false;
|
||||
await syncEditor();
|
||||
}
|
||||
async function edit(item: FinancialDisclosureListItem) {
|
||||
busy.value = true;
|
||||
try {
|
||||
const result = await fetchFinancialDisclosure(item.publicId);
|
||||
fill(result.disclosure);
|
||||
editing.value = true;
|
||||
previewing.value = false;
|
||||
await syncEditor();
|
||||
} catch (error) {
|
||||
message.value = errorText(error);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
function fill(item: FinancialDisclosureDetail) {
|
||||
Object.assign(form, item);
|
||||
}
|
||||
async function syncEditor() {
|
||||
await nextTick();
|
||||
if (editor.value) editor.value.innerHTML = form.richText;
|
||||
}
|
||||
function command(name: string, value?: string) {
|
||||
editor.value?.focus();
|
||||
document.execCommand(name, false, value);
|
||||
form.richText = editor.value?.innerHTML ?? form.richText;
|
||||
}
|
||||
function inputEditor() {
|
||||
form.richText = editor.value?.innerHTML ?? '';
|
||||
}
|
||||
async function importDocx(event: Event) {
|
||||
const input = event.target as HTMLInputElement,
|
||||
file = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
if (!file.name.toLowerCase().endsWith('.docx')) {
|
||||
message.value = '请选择 .docx 格式的 Word 文档。';
|
||||
return;
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.value = 'DOCX 文件不能超过 10MB。';
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
try {
|
||||
const mammoth = await import('mammoth/mammoth.browser');
|
||||
const result = await mammoth.convertToHtml({ arrayBuffer: await file.arrayBuffer() });
|
||||
form.richText = sanitizeRichText(result.value);
|
||||
if (!form.title) form.title = file.name.replace(/\.docx$/i, '').slice(0, 200);
|
||||
if (!form.summary) form.summary = defaultFinancialSummary(result.value);
|
||||
await syncEditor();
|
||||
message.value = result.messages.length
|
||||
? '文档已导入,部分复杂样式已简化,请预览确认。'
|
||||
: '文档已导入,可继续编辑和排版。';
|
||||
} catch {
|
||||
message.value = 'Word 文档无法解析,请确认文件未损坏。';
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
form.richText = sanitizeRichText(editor.value?.innerHTML ?? form.richText);
|
||||
if (!form.title.trim() || !defaultFinancialSummary(form.richText)) {
|
||||
message.value = '请填写标题和正文。';
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
...(form.summary.trim() ? { summary: form.summary.trim() } : {}),
|
||||
richText: form.richText,
|
||||
isPinned: form.isPinned,
|
||||
sortOrder: Number(form.sortOrder) || 0,
|
||||
};
|
||||
const result = form.publicId
|
||||
? await updateFinancialDisclosure(form.publicId, {
|
||||
expectedRevision: form.revision,
|
||||
...payload,
|
||||
})
|
||||
: await createFinancialDisclosure({ clientRequestId: crypto.randomUUID(), ...payload });
|
||||
fill(result.disclosure);
|
||||
message.value = '财务公开草稿已保存。';
|
||||
await load(page.value);
|
||||
} catch (error) {
|
||||
message.value = errorText(error);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
async function transition(item: FinancialDisclosureListItem) {
|
||||
const targetStatus = item.status === 'DRAFT' ? 'PUBLISHED' : 'ARCHIVED';
|
||||
if (
|
||||
!window.confirm(
|
||||
targetStatus === 'PUBLISHED'
|
||||
? `确认发布“${item.title}”?发布后小程序将立即可见。`
|
||||
: `确认归档“${item.title}”?`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
busy.value = true;
|
||||
try {
|
||||
await transitionFinancialDisclosure(item.publicId, {
|
||||
clientRequestId: crypto.randomUUID(),
|
||||
targetStatus,
|
||||
});
|
||||
message.value = targetStatus === 'PUBLISHED' ? '文档已发布到小程序。' : '文档已归档。';
|
||||
editing.value = false;
|
||||
await load(page.value);
|
||||
} catch (error) {
|
||||
message.value = errorText(error);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(() => load());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="content-area">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>财务公开</h1>
|
||||
<p>导入 Word 文档,调整排版后直接发布到小程序。</p>
|
||||
</div>
|
||||
<button class="primary-button compact-button" type="button" @click="newDocument">
|
||||
新建文档
|
||||
</button>
|
||||
</header>
|
||||
<p v-if="message" class="global-message" role="status">{{ message }}</p>
|
||||
<section v-if="editing" class="financial-workspace">
|
||||
<div class="financial-editor-panel">
|
||||
<div class="financial-editor-head">
|
||||
<button class="secondary-button compact-button" type="button" @click="editing = false">
|
||||
返回列表</button
|
||||
><label class="import-button"
|
||||
>导入 DOCX<input
|
||||
type="file"
|
||||
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
@change="importDocx" /></label
|
||||
><button
|
||||
class="secondary-button compact-button"
|
||||
type="button"
|
||||
@click="previewing = !previewing"
|
||||
>
|
||||
{{ previewing ? '返回编辑' : '小程序预览' }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="!previewing"
|
||||
><div class="financial-fields">
|
||||
<label>标题<input v-model.trim="form.title" maxlength="200" /></label
|
||||
><label
|
||||
>摘要(选填)<textarea v-model.trim="form.summary" maxlength="500"></textarea></label
|
||||
><label class="inline-check"
|
||||
><input v-model="form.isPinned" type="checkbox" />置顶显示</label
|
||||
><label
|
||||
>排序<input v-model.number="form.sortOrder" type="number" min="-9999" max="9999"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="rich-toolbar" aria-label="排版工具栏">
|
||||
<button type="button" @click="command('formatBlock', 'h2')">标题</button
|
||||
><button type="button" @click="command('formatBlock', 'p')">正文</button
|
||||
><button type="button" @click="command('bold')"><strong>加粗</strong></button
|
||||
><button type="button" @click="command('underline')"><u>下划线</u></button
|
||||
><button type="button" @click="command('insertUnorderedList')">项目符号</button
|
||||
><button type="button" @click="command('insertOrderedList')">编号</button
|
||||
><button type="button" @click="command('justifyLeft')">左对齐</button
|
||||
><button type="button" @click="command('justifyCenter')">居中</button
|
||||
><button type="button" @click="command('undo')">撤销</button
|
||||
><button type="button" @click="command('redo')">重做</button>
|
||||
</div>
|
||||
<div
|
||||
ref="editor"
|
||||
class="rich-editor"
|
||||
contenteditable="true"
|
||||
role="textbox"
|
||||
aria-label="财务公开正文"
|
||||
aria-multiline="true"
|
||||
@input="inputEditor"
|
||||
></div
|
||||
></template>
|
||||
<article v-else class="mobile-preview">
|
||||
<p class="preview-label">小程序阅读效果预览</p>
|
||||
<h1>{{ form.title }}</h1>
|
||||
<p v-if="form.summary" class="preview-summary">{{ form.summary }}</p>
|
||||
<div class="public-rich-text" v-html="form.richText"></div>
|
||||
</article>
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="primary-button compact-button"
|
||||
:disabled="busy"
|
||||
type="button"
|
||||
@click="save"
|
||||
>
|
||||
{{ busy ? '保存中…' : '保存草稿' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<template v-else
|
||||
><form class="filter-panel" @submit.prevent="load(1)">
|
||||
<div class="filter-field">
|
||||
<label>状态</label
|
||||
><select v-model="filter">
|
||||
<option value="">全部</option>
|
||||
<option v-for="status in FINANCIAL_DISCLOSURE_STATUSES" :key="status" :value="status">
|
||||
{{ FINANCIAL_DISCLOSURE_STATUS_LABELS[status] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="primary-button compact-button">查询</button>
|
||||
</form>
|
||||
<section v-if="loading" class="panel">正在加载财务公开文档…</section>
|
||||
<section v-else-if="!items.length" class="empty-panel">
|
||||
<h2>暂无财务公开文档</h2>
|
||||
<p>可以新建空白文档,也可以导入本地 DOCX。</p>
|
||||
</section>
|
||||
<section v-else class="table-panel">
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>状态</th>
|
||||
<th>发布/更新</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items" :key="item.publicId">
|
||||
<td>
|
||||
<strong>{{ item.title }}</strong
|
||||
><span v-if="item.isPinned" class="status-chip">置顶</span><br />{{
|
||||
item.summary || '暂无摘要'
|
||||
}}
|
||||
</td>
|
||||
<td>{{ FINANCIAL_DISCLOSURE_STATUS_LABELS[item.status] }}</td>
|
||||
<td>{{ new Date(item.publishedAt || item.updatedAt).toLocaleString('zh-CN') }}</td>
|
||||
<td>{{ item.sortOrder }}</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="item.status === 'DRAFT'"
|
||||
class="link-button"
|
||||
:disabled="busy"
|
||||
@click="edit(item)"
|
||||
>
|
||||
编辑</button
|
||||
><button
|
||||
v-if="item.status !== 'ARCHIVED'"
|
||||
class="link-button"
|
||||
:disabled="busy"
|
||||
@click="transition(item)"
|
||||
>
|
||||
{{ item.status === 'DRAFT' ? '发布' : '归档' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button :disabled="page <= 1" @click="load(page - 1)">上一页</button
|
||||
><span>第 {{ page }} / {{ totalPages }} 页</span
|
||||
><button :disabled="page >= totalPages" @click="load(page + 1)">下一页</button>
|
||||
</div>
|
||||
</section></template
|
||||
>
|
||||
</main>
|
||||
</template>
|
||||
@@ -31,13 +31,15 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"mariadb": "^3.5.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
"rxjs": "^7.8.2",
|
||||
"sanitize-html": "^2.17.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "^11.1.6",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"dotenv": "^17.4.2",
|
||||
"prisma": "^7.9.1",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
CREATE TABLE `financial_disclosures` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`public_id` CHAR(36) NOT NULL,
|
||||
`client_request_id` CHAR(36) NOT NULL,
|
||||
`request_fingerprint` CHAR(64) NOT NULL,
|
||||
`title` VARCHAR(200) NOT NULL,
|
||||
`summary` VARCHAR(500) NULL,
|
||||
`rich_text` MEDIUMTEXT NOT NULL,
|
||||
`status` ENUM('DRAFT','PUBLISHED','ARCHIVED') NOT NULL DEFAULT 'DRAFT',
|
||||
`is_pinned` BOOLEAN NOT NULL DEFAULT false,
|
||||
`sort_order` INTEGER NOT NULL DEFAULT 0,
|
||||
`revision` INTEGER UNSIGNED NOT NULL DEFAULT 1,
|
||||
`published_at` DATETIME(3) NULL,
|
||||
`created_by` BIGINT UNSIGNED NOT NULL,
|
||||
`updated_by` BIGINT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
UNIQUE INDEX `financial_disclosures_public_id_key` (`public_id`),
|
||||
UNIQUE INDEX `financial_disclosures_creator_request_key` (`created_by`, `client_request_id`),
|
||||
INDEX `financial_disclosures_public_list_idx` (`status`, `is_pinned`, `sort_order`, `published_at`),
|
||||
INDEX `financial_disclosures_updated_at_idx` (`updated_at`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `financial_disclosure_status_history` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`financial_disclosure_id` BIGINT UNSIGNED NOT NULL,
|
||||
`request_id` CHAR(36) NOT NULL,
|
||||
`request_fingerprint` CHAR(64) NOT NULL,
|
||||
`from_status` ENUM('DRAFT','PUBLISHED','ARCHIVED') NULL,
|
||||
`to_status` ENUM('DRAFT','PUBLISHED','ARCHIVED') NOT NULL,
|
||||
`operator_user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
UNIQUE INDEX `financial_disclosure_status_history_request_id_key` (`request_id`),
|
||||
INDEX `financial_disclosure_history_document_idx` (`financial_disclosure_id`, `created_at`),
|
||||
INDEX `financial_disclosure_history_operator_idx` (`operator_user_id`, `created_at`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE `financial_disclosures` ADD CONSTRAINT `financial_disclosures_created_by_fkey` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE `financial_disclosures` ADD CONSTRAINT `financial_disclosures_updated_by_fkey` FOREIGN KEY (`updated_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE `financial_disclosure_status_history` ADD CONSTRAINT `financial_disclosure_history_document_fkey` FOREIGN KEY (`financial_disclosure_id`) REFERENCES `financial_disclosures` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE `financial_disclosure_status_history` ADD CONSTRAINT `financial_disclosure_history_operator_fkey` FOREIGN KEY (`operator_user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
INSERT INTO `permissions` (`code`, `name`, `description`) VALUES
|
||||
('financial_disclosures:read', '查看财务公开', '查看财务公开草稿与发布状态'),
|
||||
('financial_disclosures:manage', '管理财务公开', '创建和编辑财务公开文档'),
|
||||
('financial_disclosures:publish', '发布财务公开', '发布和归档财务公开文档');
|
||||
|
||||
INSERT INTO `role_permissions` (`role_id`, `permission_id`)
|
||||
SELECT r.id, p.id FROM `roles` r CROSS JOIN `permissions` p
|
||||
WHERE r.code IN ('ADMIN','SUPER_ADMIN')
|
||||
AND p.code IN ('financial_disclosures:read','financial_disclosures:manage','financial_disclosures:publish');
|
||||
@@ -170,6 +170,12 @@ enum ContentPostStatus {
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum FinancialDisclosureStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
APPLICATION_SUBMITTED
|
||||
APPLICATION_ACCEPTED
|
||||
@@ -316,6 +322,9 @@ model User {
|
||||
contentStatusChanges ContentPostStatusHistory[] @relation("ContentStatusOperator")
|
||||
notifications Notification[]
|
||||
notificationPreferences NotificationPreference[]
|
||||
createdFinancialDisclosures FinancialDisclosure[] @relation("FinancialDisclosureCreator")
|
||||
updatedFinancialDisclosures FinancialDisclosure[] @relation("FinancialDisclosureUpdater")
|
||||
financialDisclosureStatusChanges FinancialDisclosureStatusHistory[] @relation("FinancialDisclosureStatusOperator")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -1148,6 +1157,50 @@ model ContentPostStatusHistory {
|
||||
@@map("content_post_status_history")
|
||||
}
|
||||
|
||||
model FinancialDisclosure {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
publicId String @unique @map("public_id") @db.Char(36)
|
||||
clientRequestId String @map("client_request_id") @db.Char(36)
|
||||
requestFingerprint String @map("request_fingerprint") @db.Char(64)
|
||||
title String @db.VarChar(200)
|
||||
summary String? @db.VarChar(500)
|
||||
richText String @map("rich_text") @db.MediumText
|
||||
status FinancialDisclosureStatus @default(DRAFT)
|
||||
isPinned Boolean @default(false) @map("is_pinned")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
revision Int @default(1) @db.UnsignedInt
|
||||
publishedAt DateTime? @map("published_at") @db.DateTime(3)
|
||||
createdById BigInt @map("created_by") @db.UnsignedBigInt
|
||||
updatedById BigInt @map("updated_by") @db.UnsignedBigInt
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
creator User @relation("FinancialDisclosureCreator", fields: [createdById], references: [id], onDelete: Restrict)
|
||||
updater User @relation("FinancialDisclosureUpdater", fields: [updatedById], references: [id], onDelete: Restrict)
|
||||
statusHistory FinancialDisclosureStatusHistory[]
|
||||
|
||||
@@unique([createdById, clientRequestId], map: "financial_disclosures_creator_request_key")
|
||||
@@index([status, isPinned, sortOrder, publishedAt], map: "financial_disclosures_public_list_idx")
|
||||
@@index([updatedAt], map: "financial_disclosures_updated_at_idx")
|
||||
@@map("financial_disclosures")
|
||||
}
|
||||
|
||||
model FinancialDisclosureStatusHistory {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
financialDisclosureId BigInt @map("financial_disclosure_id") @db.UnsignedBigInt
|
||||
requestId String @unique @map("request_id") @db.Char(36)
|
||||
requestFingerprint String @map("request_fingerprint") @db.Char(64)
|
||||
fromStatus FinancialDisclosureStatus? @map("from_status")
|
||||
toStatus FinancialDisclosureStatus @map("to_status")
|
||||
operatorUserId BigInt @map("operator_user_id") @db.UnsignedBigInt
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
financialDisclosure FinancialDisclosure @relation(fields: [financialDisclosureId], references: [id], onDelete: Restrict)
|
||||
operator User @relation("FinancialDisclosureStatusOperator", fields: [operatorUserId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([financialDisclosureId, createdAt], map: "financial_disclosure_history_document_idx")
|
||||
@@index([operatorUserId, createdAt], map: "financial_disclosure_history_operator_idx")
|
||||
@@map("financial_disclosure_status_history")
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
publicId String @unique @map("public_id") @db.Char(36)
|
||||
|
||||
@@ -25,6 +25,7 @@ import { StatisticsModule } from './statistics/statistics.module';
|
||||
import { RequestMetricsService } from './common/request-metrics.service';
|
||||
import { RequestMetricsInterceptor } from './common/request-metrics.interceptor';
|
||||
import { SystemMetricsController } from './common/system-metrics.controller';
|
||||
import { FinancialDisclosuresModule } from './financial-disclosures/financial-disclosures.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -42,6 +43,7 @@ import { SystemMetricsController } from './common/system-metrics.controller';
|
||||
ParticipationModule,
|
||||
ContentNotificationsModule,
|
||||
StatisticsModule,
|
||||
FinancialDisclosuresModule,
|
||||
],
|
||||
controllers: [HealthController, SystemMetricsController],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Length,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { FINANCIAL_DISCLOSURE_STATUSES } from '@lehuo/contracts';
|
||||
import { PaginationDto } from '../../case-management/dto/case-management.dto';
|
||||
|
||||
export class FinancialDisclosureQueryDto extends PaginationDto {
|
||||
@IsOptional() @IsIn(FINANCIAL_DISCLOSURE_STATUSES) status?: string;
|
||||
}
|
||||
export class CreateFinancialDisclosureDto {
|
||||
@IsUUID('4') clientRequestId!: string;
|
||||
@IsString() @Length(2, 200) title!: string;
|
||||
@IsOptional() @IsString() @Length(0, 500) summary?: string;
|
||||
@IsString() @Length(1, 1_000_000) richText!: string;
|
||||
@IsOptional() @IsBoolean() isPinned?: boolean;
|
||||
@IsOptional() @Type(() => Number) @IsInt() @Min(-9999) @Max(9999) sortOrder?: number;
|
||||
}
|
||||
export class UpdateFinancialDisclosureDto {
|
||||
@Type(() => Number) @IsInt() @Min(1) expectedRevision!: number;
|
||||
@IsString() @Length(2, 200) title!: string;
|
||||
@IsOptional() @IsString() @Length(0, 500) summary?: string;
|
||||
@IsString() @Length(1, 1_000_000) richText!: string;
|
||||
@IsOptional() @IsBoolean() isPinned?: boolean;
|
||||
@IsOptional() @Type(() => Number) @IsInt() @Min(-9999) @Max(9999) sortOrder?: number;
|
||||
}
|
||||
export class TransitionFinancialDisclosureDto {
|
||||
@IsUUID('4') clientRequestId!: string;
|
||||
@IsIn(['PUBLISHED', 'ARCHIVED']) targetStatus!: 'PUBLISHED' | 'ARCHIVED';
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { PERMISSION_CODES } from '@lehuo/contracts';
|
||||
import { RequireSessionChannel } from '../auth/decorators/require-session-channel.decorator';
|
||||
import { CurrentAuth } from '../common/decorators/current-auth.decorator';
|
||||
import { Public } from '../common/decorators/public.decorator';
|
||||
import { RequirePermissions } from '../common/decorators/require-permissions.decorator';
|
||||
import type { AuthContext, RequestContext } from '../common/request-context';
|
||||
import { SessionChannel } from '../generated/prisma/enums';
|
||||
import {
|
||||
CreateFinancialDisclosureDto,
|
||||
FinancialDisclosureQueryDto,
|
||||
TransitionFinancialDisclosureDto,
|
||||
UpdateFinancialDisclosureDto,
|
||||
} from './dto/financial-disclosure.dto';
|
||||
import { FinancialDisclosuresService } from './financial-disclosures.service';
|
||||
|
||||
const strict = (expectedType: new () => object) =>
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
expectedType,
|
||||
});
|
||||
|
||||
@Controller('api/v1/financial-disclosures')
|
||||
@Public()
|
||||
export class PublicFinancialDisclosuresController {
|
||||
constructor(
|
||||
@Inject(FinancialDisclosuresService) private readonly service: FinancialDisclosuresService,
|
||||
) {}
|
||||
@Get() list(@Query(strict(FinancialDisclosureQueryDto)) q: FinancialDisclosureQueryDto) {
|
||||
return this.service.listPublic(q);
|
||||
}
|
||||
@Get(':publicId') get(@Param('publicId') id: string) {
|
||||
return this.service.getPublic(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('api/v1/admin/financial-disclosures')
|
||||
@RequireSessionChannel(SessionChannel.ADMIN)
|
||||
export class AdminFinancialDisclosuresController {
|
||||
constructor(
|
||||
@Inject(FinancialDisclosuresService) private readonly service: FinancialDisclosuresService,
|
||||
) {}
|
||||
@Get() @RequirePermissions(PERMISSION_CODES.FINANCIAL_DISCLOSURES_READ) list(
|
||||
@Query(strict(FinancialDisclosureQueryDto)) q: FinancialDisclosureQueryDto,
|
||||
) {
|
||||
return this.service.listAdmin(q);
|
||||
}
|
||||
@Get(':publicId') @RequirePermissions(PERMISSION_CODES.FINANCIAL_DISCLOSURES_READ) get(
|
||||
@Param('publicId') id: string,
|
||||
) {
|
||||
return this.service.getAdmin(id);
|
||||
}
|
||||
@Post() @RequirePermissions(PERMISSION_CODES.FINANCIAL_DISCLOSURES_MANAGE) create(
|
||||
@Body(strict(CreateFinancialDisclosureDto)) input: CreateFinancialDisclosureDto,
|
||||
@CurrentAuth() actor: AuthContext,
|
||||
@Req() req: RequestContext,
|
||||
) {
|
||||
return this.service.create(input, actor, req);
|
||||
}
|
||||
@Patch(':publicId') @RequirePermissions(PERMISSION_CODES.FINANCIAL_DISCLOSURES_MANAGE) update(
|
||||
@Param('publicId') id: string,
|
||||
@Body(strict(UpdateFinancialDisclosureDto)) input: UpdateFinancialDisclosureDto,
|
||||
@CurrentAuth() actor: AuthContext,
|
||||
@Req() req: RequestContext,
|
||||
) {
|
||||
return this.service.update(id, input, actor, req);
|
||||
}
|
||||
@Post(':publicId/transition')
|
||||
@RequirePermissions(PERMISSION_CODES.FINANCIAL_DISCLOSURES_PUBLISH)
|
||||
transition(
|
||||
@Param('publicId') id: string,
|
||||
@Body(strict(TransitionFinancialDisclosureDto)) input: TransitionFinancialDisclosureDto,
|
||||
@CurrentAuth() actor: AuthContext,
|
||||
@Req() req: RequestContext,
|
||||
) {
|
||||
return this.service.transition(id, input, actor, req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import {
|
||||
AdminFinancialDisclosuresController,
|
||||
PublicFinancialDisclosuresController,
|
||||
} from './financial-disclosures.controller';
|
||||
import { FinancialDisclosuresService } from './financial-disclosures.service';
|
||||
@Module({
|
||||
imports: [AuditModule],
|
||||
controllers: [PublicFinancialDisclosuresController, AdminFinancialDisclosuresController],
|
||||
providers: [FinancialDisclosuresService],
|
||||
})
|
||||
export class FinancialDisclosuresModule {}
|
||||
@@ -0,0 +1,341 @@
|
||||
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import {
|
||||
API_ERROR_CODES,
|
||||
type FinancialDisclosureDetail,
|
||||
type FinancialDisclosureDetailResponse,
|
||||
type PaginatedFinancialDisclosuresResponse,
|
||||
} from '@lehuo/contracts';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { ApplicationError } from '../common/application-error';
|
||||
import type { AuthContext, RequestContext } from '../common/request-context';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import type { FinancialDisclosure, Prisma } from '../generated/prisma/client';
|
||||
import { SensitiveDataService } from '../security/sensitive-data.service';
|
||||
import type {
|
||||
CreateFinancialDisclosureDto,
|
||||
FinancialDisclosureQueryDto,
|
||||
TransitionFinancialDisclosureDto,
|
||||
UpdateFinancialDisclosureDto,
|
||||
} from './dto/financial-disclosure.dto';
|
||||
|
||||
const clean = (html: string) =>
|
||||
sanitizeHtml(html, {
|
||||
allowedTags: [
|
||||
'p',
|
||||
'br',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'strong',
|
||||
'b',
|
||||
'em',
|
||||
'i',
|
||||
'u',
|
||||
's',
|
||||
'blockquote',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
'a',
|
||||
'img',
|
||||
'hr',
|
||||
'span',
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'title'],
|
||||
img: ['src', 'alt', 'title'],
|
||||
th: ['colspan', 'rowspan'],
|
||||
td: ['colspan', 'rowspan'],
|
||||
span: ['style'],
|
||||
p: ['style'],
|
||||
h1: ['style'],
|
||||
h2: ['style'],
|
||||
h3: ['style'],
|
||||
},
|
||||
allowedStyles: {
|
||||
'*': {
|
||||
'text-align': [/^(left|right|center|justify)$/],
|
||||
color: [/^#[0-9a-f]{3,6}$/i],
|
||||
'background-color': [/^#[0-9a-f]{3,6}$/i],
|
||||
},
|
||||
},
|
||||
allowedSchemes: ['https'],
|
||||
allowedSchemesByTag: { img: ['https'] },
|
||||
enforceHtmlBoundary: true,
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class FinancialDisclosuresService {
|
||||
constructor(
|
||||
@Inject(PrismaService) private readonly prisma: PrismaService,
|
||||
@Inject(SensitiveDataService) private readonly sensitive: SensitiveDataService,
|
||||
@Inject(AuditService) private readonly audit: AuditService,
|
||||
) {}
|
||||
listAdmin(q: FinancialDisclosureQueryDto) {
|
||||
return this.list(q.status ? { status: q.status as never } : {}, q.page, q.pageSize);
|
||||
}
|
||||
listPublic(q: FinancialDisclosureQueryDto) {
|
||||
return this.list({ status: 'PUBLISHED' }, q.page, q.pageSize);
|
||||
}
|
||||
async getAdmin(id: string): Promise<FinancialDisclosureDetailResponse> {
|
||||
return { disclosure: this.detail(await this.find(id)) };
|
||||
}
|
||||
async getPublic(id: string): Promise<FinancialDisclosureDetailResponse> {
|
||||
const item = await this.find(id);
|
||||
if (item.status !== 'PUBLISHED') throw this.notFound();
|
||||
return { disclosure: this.detail(item) };
|
||||
}
|
||||
|
||||
async create(
|
||||
input: CreateFinancialDisclosureDto,
|
||||
actor: AuthContext,
|
||||
req: RequestContext,
|
||||
): Promise<FinancialDisclosureDetailResponse & { idempotentReplay: boolean }> {
|
||||
const richText = this.sanitize(input.richText),
|
||||
fp = this.fingerprint('financial-disclosure-create', { ...input, richText });
|
||||
const replay = await this.prisma.financialDisclosure.findUnique({
|
||||
where: {
|
||||
createdById_clientRequestId: {
|
||||
createdById: actor.userId,
|
||||
clientRequestId: input.clientRequestId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (replay) {
|
||||
if (replay.requestFingerprint !== fp) throw this.duplicate();
|
||||
return { disclosure: this.detail(replay), idempotentReplay: true };
|
||||
}
|
||||
const item = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.financialDisclosure.create({
|
||||
data: {
|
||||
publicId: randomUUID(),
|
||||
clientRequestId: input.clientRequestId,
|
||||
requestFingerprint: fp,
|
||||
title: input.title.trim(),
|
||||
summary: input.summary?.trim() || null,
|
||||
richText,
|
||||
isPinned: input.isPinned ?? false,
|
||||
sortOrder: input.sortOrder ?? 0,
|
||||
createdById: actor.userId,
|
||||
updatedById: actor.userId,
|
||||
},
|
||||
});
|
||||
await tx.financialDisclosureStatusHistory.create({
|
||||
data: {
|
||||
financialDisclosureId: created.id,
|
||||
requestId: input.clientRequestId,
|
||||
requestFingerprint: fp,
|
||||
toStatus: 'DRAFT',
|
||||
operatorUserId: actor.userId,
|
||||
},
|
||||
});
|
||||
await this.audit.record(
|
||||
req,
|
||||
{
|
||||
action: 'FINANCIAL_DISCLOSURE_CREATED',
|
||||
result: 'SUCCESS',
|
||||
actor,
|
||||
resourceType: 'FINANCIAL_DISCLOSURE',
|
||||
resourcePublicId: created.publicId,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return created;
|
||||
});
|
||||
return { disclosure: this.detail(item), idempotentReplay: false };
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
input: UpdateFinancialDisclosureDto,
|
||||
actor: AuthContext,
|
||||
req: RequestContext,
|
||||
): Promise<FinancialDisclosureDetailResponse> {
|
||||
const current = await this.find(id);
|
||||
if (current.status !== 'DRAFT') throw this.invalid();
|
||||
if (current.revision !== input.expectedRevision) throw this.conflict();
|
||||
const richText = this.sanitize(input.richText);
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const result = await tx.financialDisclosure.updateMany({
|
||||
where: { id: current.id, revision: input.expectedRevision, status: 'DRAFT' },
|
||||
data: {
|
||||
title: input.title.trim(),
|
||||
summary: input.summary?.trim() || null,
|
||||
richText,
|
||||
isPinned: input.isPinned ?? false,
|
||||
sortOrder: input.sortOrder ?? 0,
|
||||
updatedById: actor.userId,
|
||||
revision: { increment: 1 },
|
||||
},
|
||||
});
|
||||
if (result.count !== 1) throw this.conflict();
|
||||
await this.audit.record(
|
||||
req,
|
||||
{
|
||||
action: 'FINANCIAL_DISCLOSURE_UPDATED',
|
||||
result: 'SUCCESS',
|
||||
actor,
|
||||
resourceType: 'FINANCIAL_DISCLOSURE',
|
||||
resourcePublicId: id,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return tx.financialDisclosure.findUniqueOrThrow({ where: { id: current.id } });
|
||||
});
|
||||
return { disclosure: this.detail(updated) };
|
||||
}
|
||||
|
||||
async transition(
|
||||
id: string,
|
||||
input: TransitionFinancialDisclosureDto,
|
||||
actor: AuthContext,
|
||||
req: RequestContext,
|
||||
) {
|
||||
const current = await this.find(id),
|
||||
fp = this.fingerprint('financial-disclosure-transition', {
|
||||
id,
|
||||
targetStatus: input.targetStatus,
|
||||
});
|
||||
const replay = await this.prisma.financialDisclosureStatusHistory.findUnique({
|
||||
where: { requestId: input.clientRequestId },
|
||||
include: { financialDisclosure: true },
|
||||
});
|
||||
if (replay) {
|
||||
if (replay.requestFingerprint !== fp) throw this.duplicate();
|
||||
return { disclosure: this.detail(replay.financialDisclosure), idempotentReplay: true };
|
||||
}
|
||||
const allowed =
|
||||
(current.status === 'DRAFT' && input.targetStatus === 'PUBLISHED') ||
|
||||
(current.status === 'PUBLISHED' && input.targetStatus === 'ARCHIVED');
|
||||
if (!allowed) throw this.invalid();
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const result = await tx.financialDisclosure.updateMany({
|
||||
where: { id: current.id, status: current.status },
|
||||
data: {
|
||||
status: input.targetStatus,
|
||||
updatedById: actor.userId,
|
||||
revision: { increment: 1 },
|
||||
...(input.targetStatus === 'PUBLISHED' ? { publishedAt: new Date() } : {}),
|
||||
},
|
||||
});
|
||||
if (result.count !== 1) throw this.conflict();
|
||||
await tx.financialDisclosureStatusHistory.create({
|
||||
data: {
|
||||
financialDisclosureId: current.id,
|
||||
requestId: input.clientRequestId,
|
||||
requestFingerprint: fp,
|
||||
fromStatus: current.status,
|
||||
toStatus: input.targetStatus,
|
||||
operatorUserId: actor.userId,
|
||||
},
|
||||
});
|
||||
await this.audit.record(
|
||||
req,
|
||||
{
|
||||
action: 'FINANCIAL_DISCLOSURE_STATUS_CHANGED',
|
||||
result: 'SUCCESS',
|
||||
actor,
|
||||
resourceType: 'FINANCIAL_DISCLOSURE',
|
||||
resourcePublicId: id,
|
||||
metadata: { fromStatus: current.status, toStatus: input.targetStatus },
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return tx.financialDisclosure.findUniqueOrThrow({ where: { id: current.id } });
|
||||
});
|
||||
return { disclosure: this.detail(updated), idempotentReplay: false };
|
||||
}
|
||||
|
||||
private async list(
|
||||
where: Prisma.FinancialDisclosureWhereInput,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<PaginatedFinancialDisclosuresResponse> {
|
||||
const [total, items] = await this.prisma.$transaction([
|
||||
this.prisma.financialDisclosure.count({ where }),
|
||||
this.prisma.financialDisclosure.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ sortOrder: 'desc' },
|
||||
{ publishedAt: 'desc' },
|
||||
{ updatedAt: 'desc' },
|
||||
],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
]);
|
||||
return { items: items.map((i) => this.item(i)), page, pageSize, total };
|
||||
}
|
||||
private async find(publicId: string) {
|
||||
const item = await this.prisma.financialDisclosure.findUnique({ where: { publicId } });
|
||||
if (!item) throw this.notFound();
|
||||
return item;
|
||||
}
|
||||
private item(i: FinancialDisclosure) {
|
||||
return {
|
||||
publicId: i.publicId,
|
||||
title: i.title,
|
||||
summary: i.summary,
|
||||
status: i.status,
|
||||
isPinned: i.isPinned,
|
||||
sortOrder: i.sortOrder,
|
||||
revision: i.revision,
|
||||
publishedAt: i.publishedAt?.toISOString() ?? null,
|
||||
updatedAt: i.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
private detail(i: FinancialDisclosure): FinancialDisclosureDetail {
|
||||
return { ...this.item(i), richText: i.richText };
|
||||
}
|
||||
private sanitize(value: string) {
|
||||
const result = clean(value).trim();
|
||||
if (!result || !sanitizeHtml(result, { allowedTags: [], allowedAttributes: {} }).trim())
|
||||
throw new ApplicationError(
|
||||
API_ERROR_CODES.VALIDATION_ERROR,
|
||||
'财务公开正文不能为空',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
private fingerprint(domain: string, value: object) {
|
||||
return this.sensitive.lookupHash(domain, JSON.stringify(value));
|
||||
}
|
||||
private notFound() {
|
||||
return new ApplicationError(
|
||||
API_ERROR_CODES.RESOURCE_NOT_FOUND,
|
||||
'未找到该财务公开文档',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
private invalid() {
|
||||
return new ApplicationError(
|
||||
API_ERROR_CODES.INVALID_STATE_TRANSITION,
|
||||
'当前文档状态不能执行该操作',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
private conflict() {
|
||||
return new ApplicationError(
|
||||
API_ERROR_CODES.RESOURCE_CONFLICT,
|
||||
'文档已更新,请刷新后重试',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
private duplicate() {
|
||||
return new ApplicationError(
|
||||
API_ERROR_CODES.DUPLICATE_REQUEST,
|
||||
'请求标识已用于其他操作',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ describe('Phase 1 authentication and authorization', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
|
||||
@@ -106,6 +106,8 @@ describe('Phase 3 beneficiary and case management', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
await prisma.notificationPreference.deleteMany();
|
||||
|
||||
@@ -51,6 +51,8 @@ describe('Phase 6 content and notifications', () => {
|
||||
passwords = app.get(PasswordService);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ValidationPipe, type INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import type { Server } from 'node:http';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
ApiErrorResponse,
|
||||
ApiSuccessResponse,
|
||||
FinancialDisclosureDetailResponse,
|
||||
PaginatedFinancialDisclosuresResponse,
|
||||
} from '@lehuo/contracts';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { PasswordService } from '../src/auth/password.service';
|
||||
import { ADMIN_SESSION_COOKIE } from '../src/auth/session.constants';
|
||||
import { PrismaService } from '../src/database/prisma.service';
|
||||
|
||||
const ORIGIN = 'http://localhost:5173';
|
||||
const body = <T>(response: { body: unknown }) => response.body as T;
|
||||
function sessionCookie(response: { headers: Record<string, unknown> }) {
|
||||
const values = response.headers['set-cookie'];
|
||||
const cookies: string[] = Array.isArray(values)
|
||||
? values.filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
const found = cookies.find((value) => value.startsWith(`${ADMIN_SESSION_COOKIE}=`));
|
||||
if (!found) throw new Error('no cookie');
|
||||
return found.split(';')[0]!;
|
||||
}
|
||||
|
||||
describe('financial disclosure publishing', () => {
|
||||
let app: INestApplication, prisma: PrismaService, passwords: PasswordService;
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(cookieParser());
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }),
|
||||
);
|
||||
await app.init();
|
||||
prisma = app.get(PrismaService);
|
||||
passwords = app.get(PasswordService);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.authSession.deleteMany();
|
||||
await prisma.userRole.deleteMany();
|
||||
await prisma.adminCredential.deleteMany();
|
||||
await prisma.staffProfile.deleteMany();
|
||||
});
|
||||
afterAll(() => app.close());
|
||||
async function admin() {
|
||||
const username = `finance.${crypto.randomUUID()}`;
|
||||
const role = await prisma.role.findUniqueOrThrow({ where: { code: 'ADMIN' } });
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
publicId: crypto.randomUUID(),
|
||||
userType: 'STAFF',
|
||||
staffProfile: { create: { displayName: '财务公开管理员' } },
|
||||
adminCredential: {
|
||||
create: { username, passwordHash: await passwords.hash('correct horse battery staple') },
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.userRole.create({ data: { userId: user.id, roleId: role.id } });
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post('/api/v1/admin/auth/login')
|
||||
.set('origin', ORIGIN)
|
||||
.send({ username, password: 'correct horse battery staple' })
|
||||
.expect(201);
|
||||
return sessionCookie(response);
|
||||
}
|
||||
|
||||
it('keeps drafts private, sanitizes rich text, publishes and audits the document', async () => {
|
||||
const cookie = await admin();
|
||||
const created = await request(app.getHttpServer() as Server)
|
||||
.post('/api/v1/admin/financial-disclosures')
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({
|
||||
clientRequestId: crypto.randomUUID(),
|
||||
title: '2026年度财务公开',
|
||||
summary: '年度财务公开说明',
|
||||
richText: '<h2>年度说明</h2><p onclick="alert(1)">公开正文</p><script>alert(1)</script>',
|
||||
isPinned: true,
|
||||
sortOrder: 10,
|
||||
})
|
||||
.expect(201);
|
||||
const disclosure =
|
||||
body<ApiSuccessResponse<FinancialDisclosureDetailResponse>>(created).data.disclosure;
|
||||
expect(disclosure.richText).not.toMatch(/script|onclick/);
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/api/v1/financial-disclosures/${disclosure.publicId}`)
|
||||
.expect(404);
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/api/v1/admin/financial-disclosures/${disclosure.publicId}/transition`)
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({ clientRequestId: crypto.randomUUID(), targetStatus: 'PUBLISHED' })
|
||||
.expect(201);
|
||||
const listed = await request(app.getHttpServer() as Server)
|
||||
.get('/api/v1/financial-disclosures?page=1&pageSize=20')
|
||||
.expect(200);
|
||||
expect(
|
||||
body<ApiSuccessResponse<PaginatedFinancialDisclosuresResponse>>(listed).data.items,
|
||||
).toHaveLength(1);
|
||||
expect(await prisma.financialDisclosureStatusHistory.count()).toBe(2);
|
||||
expect(await prisma.auditLog.count({ where: { resourceType: 'FINANCIAL_DISCLOSURE' } })).toBe(
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects edits after publishing and hides archived documents', async () => {
|
||||
const cookie = await admin(),
|
||||
requestId = crypto.randomUUID();
|
||||
const created = await request(app.getHttpServer() as Server)
|
||||
.post('/api/v1/admin/financial-disclosures')
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({
|
||||
clientRequestId: requestId,
|
||||
title: '专项财务公开',
|
||||
richText: '<p>专项公开正文内容</p>',
|
||||
})
|
||||
.expect(201);
|
||||
const item =
|
||||
body<ApiSuccessResponse<FinancialDisclosureDetailResponse>>(created).data.disclosure;
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/api/v1/admin/financial-disclosures/${item.publicId}/transition`)
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({ clientRequestId: crypto.randomUUID(), targetStatus: 'PUBLISHED' })
|
||||
.expect(201);
|
||||
const rejected = await request(app.getHttpServer() as Server)
|
||||
.patch(`/api/v1/admin/financial-disclosures/${item.publicId}`)
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({
|
||||
expectedRevision: item.revision + 1,
|
||||
title: item.title,
|
||||
richText: '<p>覆盖已发布内容</p>',
|
||||
})
|
||||
.expect(409);
|
||||
expect(body<ApiErrorResponse>(rejected).error.code).toBe('INVALID_STATE_TRANSITION');
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/api/v1/admin/financial-disclosures/${item.publicId}/transition`)
|
||||
.set('origin', ORIGIN)
|
||||
.set('cookie', cookie)
|
||||
.send({ clientRequestId: crypto.randomUUID(), targetStatus: 'ARCHIVED' })
|
||||
.expect(201);
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/api/v1/financial-disclosures/${item.publicId}`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,8 @@ describe('Phase 4 follow-up closure', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
await prisma.notificationPreference.deleteMany();
|
||||
|
||||
@@ -70,6 +70,8 @@ describe('Phase 5 projects, activities and volunteers', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
throttlerStorage.storage.clear();
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
|
||||
@@ -69,6 +69,8 @@ describe('Phase 2 service applications', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.financialDisclosureStatusHistory.deleteMany();
|
||||
await prisma.financialDisclosure.deleteMany();
|
||||
await prisma.notificationDelivery.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
await prisma.notificationPreference.deleteMany();
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"pages/volunteer-center/volunteer-center",
|
||||
"pages/content/content",
|
||||
"pages/content-detail/content-detail",
|
||||
"pages/notifications/notifications"
|
||||
"pages/notifications/notifications",
|
||||
"pages/financial-disclosures/financial-disclosures",
|
||||
"pages/financial-disclosure-detail/financial-disclosure-detail"
|
||||
],
|
||||
"window": {
|
||||
"backgroundColor": "#F7F5F2",
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "navigationBarTitleText": "财务公开详情" }
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import type { FinancialDisclosureDetail } from '@lehuo/contracts';
|
||||
import { fetchFinancialDisclosure } from '../../services/financial-disclosures';
|
||||
Page({
|
||||
data: { loading: true, message: '', disclosure: null as FinancialDisclosureDetail | null },
|
||||
onLoad(options) {
|
||||
const id = String(options['publicId'] || '');
|
||||
if (!id) {
|
||||
this.setData({ loading: false, message: '缺少财务公开文档标识,请返回重试' });
|
||||
return;
|
||||
}
|
||||
void this.load(id);
|
||||
},
|
||||
async load(id: string) {
|
||||
this.setData({ loading: true, message: '' });
|
||||
try {
|
||||
const result = await fetchFinancialDisclosure(id);
|
||||
this.setData({ disclosure: result.disclosure });
|
||||
} catch (error) {
|
||||
this.setData({ message: error instanceof Error ? error.message : '财务公开内容加载失败' });
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
<view class="page disclosure-detail-page"><view wx:if="{{loading}}" class="state-card">正在加载…</view><view wx:elif="{{message}}" class="state-card"><text class="state-title">文档暂时无法查看</text><text class="state-text">{{message}}</text></view><view wx:elif="{{disclosure}}" class="disclosure-paper"><text class="disclosure-title">{{disclosure.title}}</text><text wx:if="{{disclosure.summary}}" class="disclosure-summary">{{disclosure.summary}}</text><view class="disclosure-meta"><text>发布时间</text><text>{{disclosure.publishedAt}}</text></view><scroll-view class="document-scroll" scroll-x="true"><rich-text class="disclosure-rich-text" nodes="{{disclosure.richText}}" selectable="true"></rich-text></scroll-view><text class="disclosure-note">本页面内容由乐活公益管理后台正式发布。</text></view></view>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
@import '../../app.wxss';
|
||||
.disclosure-paper {
|
||||
padding: 38rpx 32rpx;
|
||||
border-radius: 26rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 10rpx 30rpx rgba(38, 42, 48, 0.07);
|
||||
}
|
||||
.disclosure-title,
|
||||
.disclosure-summary,
|
||||
.disclosure-note {
|
||||
display: block;
|
||||
}
|
||||
.disclosure-title {
|
||||
color: #25292f;
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.disclosure-summary {
|
||||
margin-top: 22rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff5ef;
|
||||
color: #684d40;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.disclosure-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 28rpx 0;
|
||||
padding-bottom: 22rpx;
|
||||
border-bottom: 1rpx solid #eceef1;
|
||||
color: #8c929a;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.document-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
.disclosure-rich-text {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
color: #333840;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.disclosure-note {
|
||||
margin-top: 36rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #eceef1;
|
||||
color: #9a9fa7;
|
||||
font-size: 23rpx;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "navigationBarTitleText": "财务公开" }
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { FinancialDisclosureListItem } from '@lehuo/contracts';
|
||||
import { fetchFinancialDisclosures } from '../../services/financial-disclosures';
|
||||
Page({
|
||||
data: { loading: true, message: '', items: [] as FinancialDisclosureListItem[] },
|
||||
onLoad() {
|
||||
void this.load();
|
||||
},
|
||||
async load() {
|
||||
this.setData({ loading: true, message: '' });
|
||||
try {
|
||||
const result = await fetchFinancialDisclosures();
|
||||
this.setData({ items: result.items });
|
||||
} catch (error) {
|
||||
this.setData({ message: error instanceof Error ? error.message : '财务公开内容加载失败' });
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
open(event: WechatMiniprogram.BaseEvent) {
|
||||
const id = String(event.currentTarget.dataset['id'] || '');
|
||||
if (id)
|
||||
void wx.navigateTo({
|
||||
url: `/pages/financial-disclosure-detail/financial-disclosure-detail?publicId=${encodeURIComponent(id)}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<view class="page financial-list-page"><view class="financial-hero"><text class="financial-hero__eyebrow">公开 · 透明 · 可查阅</text><text class="financial-hero__title">财务公开</text><text class="financial-hero__copy">查看乐活公益经管理端正式发布的财务公开文档。</text></view><view wx:if="{{loading}}" class="state-card">正在加载…</view><view wx:elif="{{message}}" class="state-card"><text class="state-title">暂时无法加载</text><text class="state-text">{{message}}</text><button class="secondary-button" bindtap="load">重新加载</button></view><view wx:elif="{{!items.length}}" class="state-card"><text class="state-title">暂无公开文档</text><text class="state-text">财务公开文档发布后会展示在这里。</text></view><button wx:for="{{items}}" wx:key="publicId" class="financial-card" data-id="{{item.publicId}}" bindtap="open"><view class="financial-card__top"><text wx:if="{{item.isPinned}}" class="pin-tag">置顶</text><text class="financial-card__date">{{item.publishedAt}}</text></view><text class="financial-card__title">{{item.title}}</text><text class="financial-card__summary">{{item.summary || '点击查看完整公开内容'}}</text><text class="financial-card__link">查看详情 ›</text></button></view>
|
||||
@@ -0,0 +1,74 @@
|
||||
@import '../../app.wxss';
|
||||
.financial-hero {
|
||||
padding: 40rpx 34rpx;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(135deg, #ff6b35, #ff9b5f);
|
||||
color: #fff;
|
||||
box-shadow: 0 18rpx 40rpx rgba(240, 90, 40, 0.18);
|
||||
}
|
||||
.financial-hero text,
|
||||
.financial-card text {
|
||||
display: block;
|
||||
}
|
||||
.financial-hero__eyebrow {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.86;
|
||||
}
|
||||
.financial-hero__title {
|
||||
margin-top: 8rpx;
|
||||
font-size: 46rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.financial-hero__copy {
|
||||
margin-top: 14rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.financial-card {
|
||||
width: 100%;
|
||||
margin-top: 24rpx;
|
||||
padding: 30rpx;
|
||||
border: 0;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
box-shadow: 0 8rpx 28rpx rgba(38, 42, 48, 0.07);
|
||||
}
|
||||
.financial-card::after {
|
||||
border: 0;
|
||||
}
|
||||
.financial-card__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 38rpx;
|
||||
}
|
||||
.pin-tag {
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #fff0e8;
|
||||
color: #ee5b27;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.financial-card__date {
|
||||
margin-left: auto;
|
||||
color: #969ba3;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.financial-card__title {
|
||||
margin-top: 16rpx;
|
||||
color: #262a30;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.financial-card__summary {
|
||||
margin-top: 14rpx;
|
||||
color: #676d76;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.financial-card__link {
|
||||
margin-top: 20rpx;
|
||||
color: #f05a28;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -43,6 +43,9 @@ Page({
|
||||
organization() {
|
||||
void wx.navigateTo({ url: '/pages/content/content?type=ORGANIZATION_INTRO' });
|
||||
},
|
||||
financialDisclosures() {
|
||||
void wx.navigateTo({ url: '/pages/financial-disclosures/financial-disclosures' });
|
||||
},
|
||||
openProject(event: WechatMiniprogram.BaseEvent) {
|
||||
void wx.navigateTo({
|
||||
url: `/pages/project-detail/project-detail?publicId=${event.currentTarget.dataset['id'] as string}`,
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
<view wx:if="{{!activities.length}}" class="empty-strip">暂无可查看活动</view>
|
||||
<button wx:for="{{activities}}" wx:key="publicId" class="feature-card" data-id="{{item.publicId}}" bindtap="openActivity"><view class="feature-card__art green">♧</view><view class="feature-card__body"><text class="feature-card__tag">{{item.statusLabel}}</text><text class="feature-card__title">{{item.title}}</text><text class="feature-card__copy">{{item.summary}}</text><text class="feature-card__meta">已报名 {{item.activeRegistrationCount}} / {{item.capacity === null ? '不限' : item.capacity}}</text></view></button>
|
||||
<view class="section-heading"><text class="section-title">了解乐活</text><text class="section-note">所有内容均经审核发布</text></view>
|
||||
<view class="link-grid"><button bindtap="content"><text class="link-grid__icon orange">闻</text><text>公益动态</text><text>›</text></button><button bindtap="organization"><text class="link-grid__icon blue">乐</text><text>机构介绍</text><text>›</text></button></view>
|
||||
<view class="link-grid"><button bindtap="content"><text class="link-grid__icon orange">闻</text><text>公益动态</text><text>›</text></button><button bindtap="organization"><text class="link-grid__icon blue">乐</text><text>机构介绍</text><text>›</text></button><button bindtap="financialDisclosures"><text class="link-grid__icon green">财</text><text>财务公开</text><text>›</text></button></view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {
|
||||
FinancialDisclosureDetailResponse,
|
||||
PaginatedFinancialDisclosuresResponse,
|
||||
} from '@lehuo/contracts';
|
||||
import { apiRequest } from './api-client';
|
||||
export const fetchFinancialDisclosures = () =>
|
||||
apiRequest<PaginatedFinancialDisclosuresResponse>(
|
||||
'/api/v1/financial-disclosures?page=1&pageSize=50',
|
||||
);
|
||||
export const fetchFinancialDisclosure = (id: string) =>
|
||||
apiRequest<FinancialDisclosureDetailResponse>(
|
||||
`/api/v1/financial-disclosures/${encodeURIComponent(id)}`,
|
||||
);
|
||||
@@ -13,6 +13,8 @@ const allowedTags = new Set([
|
||||
'input',
|
||||
'label',
|
||||
'picker',
|
||||
'rich-text',
|
||||
'scroll-view',
|
||||
'switch',
|
||||
'text',
|
||||
'textarea',
|
||||
|
||||
@@ -452,3 +452,11 @@ V1:
|
||||
- 志愿服务时长
|
||||
|
||||
不得用手工填写数字覆盖真实统计结果。
|
||||
|
||||
---
|
||||
|
||||
## 17. 财务公开文档
|
||||
|
||||
经用户批准,财务公开采用文档发布方式,不建设财务核算或在线募捐系统。工作人员在本地完成文档后,可在管理后台导入 `.docx`、继续编辑基础富文本排版、预览并发布;公众在小程序“公益”页查看已发布文档列表和详情。
|
||||
|
||||
最小字段为标题、可选摘要、富文本正文、草稿/已发布/已归档状态、置顶、排序、revision、发布时间和创建/更新人员。首版不包含支付、捐赠订单、银行流水同步、结构化收支录入、金额统计或复杂审批。
|
||||
|
||||
@@ -470,3 +470,18 @@ Phase 7 不建立可人工修改的统计快照;九项 V1 指标直接从申
|
||||
6. 所有枚举在 contracts 中有单一事实来源。
|
||||
7. migration 不允许 destructive change 直接上线。
|
||||
8. 删除列采用“停用 → 迁移 → 验证 → 后续版本删除”。
|
||||
|
||||
---
|
||||
|
||||
## 23. financial_disclosures
|
||||
|
||||
- id / public_id(随机 UUID)
|
||||
- client_request_id / request_fingerprint(创建幂等)
|
||||
- title / summary nullable
|
||||
- rich_text(后端白名单清洗后的富文本)
|
||||
- status:DRAFT / PUBLISHED / ARCHIVED
|
||||
- is_pinned / sort_order / revision
|
||||
- published_at nullable
|
||||
- created_by / updated_by / timestamps
|
||||
|
||||
`financial_disclosure_status_history` 只追加保存创建、发布和归档事实,请求 UUID 全局唯一。已发布文档禁止覆盖编辑,只能归档;草稿编辑使用 revision 乐观锁。
|
||||
|
||||
@@ -287,3 +287,22 @@ Phase 0 提供:
|
||||
```
|
||||
|
||||
健康检查不得返回配置值、数据库连接串、密钥或其他敏感信息。业务 API 仍统一使用 `/api/v1` 前缀;基础设施健康检查不属于业务 API。
|
||||
|
||||
---
|
||||
|
||||
## 12. 财务公开文档 API
|
||||
|
||||
公众接口:
|
||||
|
||||
- `GET /api/v1/financial-disclosures`
|
||||
- `GET /api/v1/financial-disclosures/:publicId`
|
||||
|
||||
只返回 `PUBLISHED` 文档,草稿和归档文档按不存在处理。
|
||||
|
||||
管理端接口:
|
||||
|
||||
- `GET/POST /api/v1/admin/financial-disclosures`
|
||||
- `GET/PATCH /api/v1/admin/financial-disclosures/:publicId`
|
||||
- `POST /api/v1/admin/financial-disclosures/:publicId/transition`
|
||||
|
||||
创建、发布和归档使用客户端 UUID 幂等;编辑只允许草稿且要求 expectedRevision。状态为 `DRAFT → PUBLISHED → ARCHIVED`,状态变化与历史、审计事务一致。DOCX 在管理端浏览器转换为富文本,API 只接受并白名单清洗富文本,不执行 Office 文件。
|
||||
|
||||
@@ -283,3 +283,10 @@
|
||||
- 只有 `APPROVED` 志愿者可报名;活动行锁、有效报名计数和数据库唯一约束共同防止并发超额与重复报名。
|
||||
- 报名审核、取消、签到、缺席和服务分钟均写不可变历史与审计。服务分钟只允许对已通过、已签到且活动已完成的记录核定,且不得超过活动持续时间。
|
||||
- 公益项目和活动公开接口只返回公开状态内容,不返回志愿者名单或服务对象资料。封面未配置对象存储时不生成假地址或假成功。
|
||||
|
||||
## 财务公开富文本安全(2026-08-13)
|
||||
|
||||
- DOCX 仅在管理端浏览器转换,不由 API 执行或解析 Office 宏;首版不持久化原 Word 文件。
|
||||
- 管理端导入和预览先经 DOMPurify 清洗,API 保存前再次使用独立白名单清洗;禁止脚本、事件属性、iframe、非 HTTPS 链接和非 HTTPS 图片。
|
||||
- 只有管理员与超级管理员默认获得财务公开查看、编辑和发布权限;公众只能访问已发布文档。
|
||||
- 已发布文档禁止原地覆盖编辑,归档后不可公开;发布和归档写入不可变历史与审计。
|
||||
|
||||
@@ -167,3 +167,11 @@ P2:5. 公益项目 6. 公益活动 7. 我要做公益
|
||||
- 生成的人物图片均为虚构品牌场景素材,仅作服务类别与项目视觉,不作为真实服务对象、服务现场或项目成果证据;素材已压缩进入小程序本地包。
|
||||
- 捐赠支付、自动捐赠、捐赠记录/金额、物资订单、志愿者培训/证书/团队不展示入口;代码只保留禁用的集中能力登记和组件/导航扩展边界。
|
||||
- 图片中的金额、满意度、捐赠人数等示例值不得复制;所有运行时数字必须来自已批准接口,缺少事实时使用明确空状态。
|
||||
|
||||
---
|
||||
|
||||
## 13. 财务公开文档体验(2026-08-13)
|
||||
|
||||
- 管理端新增独立“财务公开”导航,支持空白新建、10MB 以内 DOCX 导入、标题/摘要、基础标题与段落、加粗、下划线、列表、对齐、撤销/重做和接近小程序宽度的预览。
|
||||
- Word 的复杂浮动对象、页眉页脚、分页和高级样式允许在导入时简化,发布前必须人工预览确认;首版重点保障段落、标题、列表和表格。
|
||||
- 小程序“公益 → 了解乐活”新增财务公开入口;列表展示置顶、标题、摘要和发布时间,详情使用 rich-text 展示,宽表可横向滚动,加载失败可重试。
|
||||
|
||||
@@ -155,3 +155,13 @@ UAT 不只让开发人员自测。
|
||||
- 微信小程序审核所需材料就绪;
|
||||
- 监控和告警正常;
|
||||
- 可回滚上一版本。
|
||||
|
||||
---
|
||||
|
||||
## 8. 财务公开验收
|
||||
|
||||
- DOCX 可在管理端本地导入,基础标题、段落、列表和表格可继续编辑并预览。
|
||||
- 危险 HTML、脚本、事件属性、iframe 和非 HTTPS 外链不能进入公开正文。
|
||||
- 未登录或无权限人员不能访问管理接口;草稿和归档文档不能从公众接口获取。
|
||||
- 草稿可编辑并使用 revision 防并发覆盖;已发布文档不能覆盖编辑,只能归档。
|
||||
- 创建、发布和归档幂等,状态历史和审计完整;小程序列表、详情、空状态和失败状态可用。
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
|
||||
2026-08-13
|
||||
|
||||
任务:财务公开文档发布模块。
|
||||
|
||||
完成:按用户确认的简化范围新增独立财务公开模块。管理后台支持新建空白文档、导入 10MB 内 `.docx`、编辑标题/摘要/置顶/排序、基础富文本排版、小程序宽度预览、保存草稿、直接发布和归档;小程序“公益”页增加财务公开入口,完成公开列表、详情、宽表横向阅读、空状态和错误状态。新增共享契约、`financial_disclosures` 与只追加状态历史、三项独立权限、随机公开 ID、创建/状态幂等、revision 乐观锁、发布/归档事务审计,以及管理端 DOMPurify + API sanitize-html 双层富文本白名单清洗。已发布内容禁止原地覆盖编辑。
|
||||
|
||||
范围:这是用户明确批准的新增模块,但不改变 D-017 对支付和公开募捐的排除。首版不保存原 DOCX、不提供结构化收支字段、财务核算、复杂审批、支付、捐赠订单、银行流水同步或金额统计。Word 复杂浮动对象、页眉页脚和高级分页样式可能简化,发布前必须人工预览。
|
||||
|
||||
验证:格式化、Lint、全仓类型检查和 28 个单元测试通过;新增 migration 已部署到专用 `lehuo_test`,共 9 个 migration;36 个真实 MySQL 集成测试全部通过,覆盖草稿隔离、危险富文本清洗、发布可见、发布后禁止覆盖、归档隐藏、状态历史和审计;小程序页面门禁通过 20 个页面,运行时 workspace 导入门禁通过;全仓生产构建通过。新增 D-018。
|
||||
|
||||
遗留:仍需在微信开发者工具清缓存重新编译并进行真机财务公开列表/详情视觉验收;需用机构实际 DOCX 样本确认复杂表格和排版还原度。真实微信 AppID/AppSecret、合法 HTTPS 域名和生产环境配置仍按既有计划后期提供。
|
||||
|
||||
---
|
||||
|
||||
2026-08-13
|
||||
|
||||
任务:微信真机二级页面白屏修复。
|
||||
|
||||
根因:服务申请和协作任务详情/服务记录页面在运行时从 workspace 包 `@lehuo/contracts` 导入业务常量。现有小程序工程没有生成 `miniprogram_npm`,常规 TypeScript 检查会擦除类型导入并通过,但真机无法解析运行时 workspace 模块,导致页面脚本在 `Page()` 注册前终止并显示空白。另发现内容详情使用非小程序原生 `article` 标签、项目详情使用 `strong` 标签,以及详情页缺少路由参数时可能持续处于加载状态。
|
||||
|
||||
@@ -209,6 +209,20 @@ V1 统计直接从业务事实表计算,不建立可人工覆盖的统计表
|
||||
|
||||
---
|
||||
|
||||
## D-018 财务公开采用受限富文本文档发布
|
||||
|
||||
状态:ACCEPTED
|
||||
|
||||
决定:
|
||||
财务公开作为独立文档发布模块,由管理人员在本地完成文档后导入管理后台、调整基础富文本排版、预览并直接发布,小程序只读查看。状态采用 `DRAFT → PUBLISHED → ARCHIVED`;已发布正文禁止覆盖编辑,创建、发布和归档必须幂等并写不可变状态历史及审计。
|
||||
|
||||
DOCX 仅在管理端浏览器转换,API 保存前对富文本执行服务端白名单清洗。首版不保存原 Word 文件,不建设结构化财务核算、复杂审核、在线募捐、支付、捐赠订单、银行流水同步或金额统计。
|
||||
|
||||
原因:
|
||||
满足机构以现有本地文档进行公开的真实工作方式,降低录入成本,并通过双层富文本清洗、后端权限和发布状态隔离保证公众展示安全;同时避免把轻量公开需求扩大成财务或支付系统。
|
||||
|
||||
---
|
||||
|
||||
## 待确认建议
|
||||
|
||||
Codex 若有会改变已批准架构的建议,只能写在这里,不得直接实施:
|
||||
|
||||
@@ -48,6 +48,9 @@ export const PERMISSION_CODES = {
|
||||
CONTENT_READ: 'content:read',
|
||||
CONTENT_MANAGE: 'content:manage',
|
||||
CONTENT_REVIEW: 'content:review',
|
||||
FINANCIAL_DISCLOSURES_READ: 'financial_disclosures:read',
|
||||
FINANCIAL_DISCLOSURES_MANAGE: 'financial_disclosures:manage',
|
||||
FINANCIAL_DISCLOSURES_PUBLISH: 'financial_disclosures:publish',
|
||||
NOTIFICATIONS_MANAGE: 'notifications:manage',
|
||||
STATISTICS_READ: 'statistics:read',
|
||||
EXPORTS_CREATE: 'exports:create',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export const FINANCIAL_DISCLOSURE_STATUSES = ['DRAFT', 'PUBLISHED', 'ARCHIVED'] as const;
|
||||
export type FinancialDisclosureStatus = (typeof FINANCIAL_DISCLOSURE_STATUSES)[number];
|
||||
|
||||
export const FINANCIAL_DISCLOSURE_STATUS_LABELS: Record<FinancialDisclosureStatus, string> = {
|
||||
DRAFT: '草稿',
|
||||
PUBLISHED: '已发布',
|
||||
ARCHIVED: '已归档',
|
||||
};
|
||||
|
||||
export interface FinancialDisclosureListItem {
|
||||
publicId: string;
|
||||
title: string;
|
||||
summary: string | null;
|
||||
status: FinancialDisclosureStatus;
|
||||
isPinned: boolean;
|
||||
sortOrder: number;
|
||||
revision: number;
|
||||
publishedAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface FinancialDisclosureDetail extends FinancialDisclosureListItem {
|
||||
richText: string;
|
||||
}
|
||||
|
||||
export interface PaginatedFinancialDisclosuresResponse {
|
||||
items: FinancialDisclosureListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface FinancialDisclosureDetailResponse {
|
||||
disclosure: FinancialDisclosureDetail;
|
||||
}
|
||||
export interface CreateFinancialDisclosureRequest {
|
||||
clientRequestId: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
richText: string;
|
||||
isPinned?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
export interface UpdateFinancialDisclosureRequest {
|
||||
expectedRevision: number;
|
||||
title: string;
|
||||
summary?: string;
|
||||
richText: string;
|
||||
isPinned?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
export interface TransitionFinancialDisclosureRequest {
|
||||
clientRequestId: string;
|
||||
targetStatus: 'PUBLISHED' | 'ARCHIVED';
|
||||
}
|
||||
@@ -27,6 +27,20 @@ export {
|
||||
SERVICE_CATEGORY_LABELS,
|
||||
} from './service-applications.js';
|
||||
export type { StatisticsOverview } from './statistics.js';
|
||||
export {
|
||||
FINANCIAL_DISCLOSURE_STATUSES,
|
||||
FINANCIAL_DISCLOSURE_STATUS_LABELS,
|
||||
} from './financial-disclosures.js';
|
||||
export type {
|
||||
FinancialDisclosureStatus,
|
||||
FinancialDisclosureListItem,
|
||||
FinancialDisclosureDetail,
|
||||
PaginatedFinancialDisclosuresResponse,
|
||||
FinancialDisclosureDetailResponse,
|
||||
CreateFinancialDisclosureRequest,
|
||||
UpdateFinancialDisclosureRequest,
|
||||
TransitionFinancialDisclosureRequest,
|
||||
} from './financial-disclosures.js';
|
||||
export {
|
||||
CONTENT_POST_STATUSES,
|
||||
CONTENT_POST_STATUS_LABELS,
|
||||
|
||||
Generated
+336
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml: 5.2.2
|
||||
lodash: 4.18.1
|
||||
|
||||
importers:
|
||||
|
||||
@@ -35,6 +36,12 @@ importers:
|
||||
'@lehuo/contracts':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/contracts
|
||||
dompurify:
|
||||
specifier: ^3.4.13
|
||||
version: 3.4.13
|
||||
mammoth:
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1
|
||||
pinia:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3(typescript@5.9.2)(vue@3.5.41(typescript@5.9.2))
|
||||
@@ -114,6 +121,9 @@ importers:
|
||||
rxjs:
|
||||
specifier: ^7.8.2
|
||||
version: 7.8.2
|
||||
sanitize-html:
|
||||
specifier: ^2.17.6
|
||||
version: 2.17.6
|
||||
devDependencies:
|
||||
'@nestjs/testing':
|
||||
specifier: ^11.1.6
|
||||
@@ -127,6 +137,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.13.3
|
||||
'@types/sanitize-html':
|
||||
specifier: ^2.16.1
|
||||
version: 2.16.1
|
||||
'@types/supertest':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
@@ -1032,6 +1045,9 @@ packages:
|
||||
'@types/react@19.2.18':
|
||||
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
|
||||
|
||||
'@types/sanitize-html@2.16.1':
|
||||
resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||
|
||||
@@ -1044,6 +1060,9 @@ packages:
|
||||
'@types/supertest@6.0.3':
|
||||
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/validator@13.15.10':
|
||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||
|
||||
@@ -1231,6 +1250,10 @@ packages:
|
||||
'@vue/shared@3.5.41':
|
||||
resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1281,6 +1304,9 @@ packages:
|
||||
resolution: {integrity: sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
argparse@1.0.3:
|
||||
resolution: {integrity: sha512-W3ljhM0j9lduyssD8+dPR0P0HWCSCGrflhsgIVFzNQpx8TDQGN/dcqcb0hOuOWsf0TNZ8OIVgKXC7Rb2vpUp+Q==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
@@ -1305,12 +1331,18 @@ packages:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
better-result@2.10.0:
|
||||
resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==}
|
||||
|
||||
birpc@2.3.0:
|
||||
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
|
||||
|
||||
bluebird@3.4.7:
|
||||
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
|
||||
|
||||
body-parser@2.3.0:
|
||||
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1450,6 +1482,9 @@ packages:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
cors@2.8.6:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -1518,6 +1553,9 @@ packages:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dayjs@1.11.21:
|
||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1538,6 +1576,10 @@ packages:
|
||||
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
defu@6.1.7:
|
||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||
|
||||
@@ -1562,10 +1604,48 @@ packages:
|
||||
dezalgo@1.0.4:
|
||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||
|
||||
dingbat-to-unicode@1.0.1:
|
||||
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
dom-serializer@3.1.1:
|
||||
resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domelementtype@3.0.0:
|
||||
resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domhandler@6.0.1:
|
||||
resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
dompurify@3.4.13:
|
||||
resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
domutils@4.0.2:
|
||||
resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
dotenv@17.4.2:
|
||||
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
duck@0.1.12:
|
||||
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1596,10 +1676,18 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
entities@4.2.0:
|
||||
resolution: {integrity: sha512-wEJa03bJgqEwPnkUqYdgmcfUXfm6+4hePQhntIvRy/1/+C4dFuhYHsgKBRjbQ6OWBh42P+VhAoCDO77DUh0e/Q==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@7.0.1:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@8.0.0:
|
||||
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
env-paths@3.0.0:
|
||||
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
@@ -1862,6 +1950,13 @@ packages:
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
htmlparser2@12.0.0:
|
||||
resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1885,6 +1980,9 @@ packages:
|
||||
resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
import-fresh@3.2.1:
|
||||
resolution: {integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1916,6 +2014,10 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
@@ -1926,6 +2028,9 @@ packages:
|
||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -1960,9 +2065,15 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
launder@1.7.1:
|
||||
resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1970,6 +2081,9 @@ packages:
|
||||
libphonenumber-js@1.13.10:
|
||||
resolution: {integrity: sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
load-esm@1.0.3:
|
||||
resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
|
||||
engines: {node: '>=13.2.0'}
|
||||
@@ -1987,6 +2101,9 @@ packages:
|
||||
long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
|
||||
lop@0.4.2:
|
||||
resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
|
||||
|
||||
loupe@3.2.1:
|
||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||
|
||||
@@ -2011,6 +2128,11 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
mammoth@1.12.1:
|
||||
resolution: {integrity: sha512-nCH9KKjWi3jQ+i8bUKs7k1yrXtSEGpWgF8IYkzsFMcbn+5S6l4bZEBbyx2hOQErFiXPuAs9RPa6qjXVxhyx/8g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mariadb@3.4.5:
|
||||
resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -2142,6 +2264,9 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
option@0.2.4:
|
||||
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2161,10 +2286,16 @@ packages:
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
pako@1.0.2:
|
||||
resolution: {integrity: sha512-qCaDHl8pk5JSbz5k1Bi5zTZf430DUuV13pmcFEHHan4fW++WIwm2HXUirpzfciBRyFUN+eeZW11bfIM/Y4Zeog==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-srcset@1.0.2:
|
||||
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2176,6 +2307,10 @@ packages:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-is-absolute@1.0.1:
|
||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2249,6 +2384,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
proper-lockfile@4.1.2:
|
||||
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
|
||||
|
||||
@@ -2287,6 +2425,9 @@ packages:
|
||||
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -2340,6 +2481,9 @@ packages:
|
||||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
@@ -2350,6 +2494,10 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sanitize-html@2.17.6:
|
||||
resolution: {integrity: sha512-M4bo9tfv1yfhQZZKkc6dL07ALrGJtfvNOuhX3hU9AVPR/uPQ+nKOJBqTYc7LfMQblTW04mtSWDJWEyLvygJsLA==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
@@ -2369,6 +2517,9 @@ packages:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
@@ -2414,6 +2565,9 @@ packages:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2443,6 +2597,9 @@ packages:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
@@ -2563,6 +2720,9 @@ packages:
|
||||
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
underscore@1.13.8:
|
||||
resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==}
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
@@ -2712,6 +2872,10 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
xmlbuilder@10.1.1:
|
||||
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
zeptomatch@2.1.0:
|
||||
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
|
||||
|
||||
@@ -3534,6 +3698,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/sanitize-html@2.16.1':
|
||||
dependencies:
|
||||
htmlparser2: 10.1.0
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 24.13.3
|
||||
@@ -3555,6 +3723,9 @@ snapshots:
|
||||
'@types/methods': 1.1.4
|
||||
'@types/superagent': 8.1.11
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
optional: true
|
||||
|
||||
'@types/validator@13.15.10': {}
|
||||
|
||||
'@types/wechat-miniprogram@3.4.10': {}
|
||||
@@ -3871,6 +4042,8 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.41': {}
|
||||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
@@ -3921,6 +4094,11 @@ snapshots:
|
||||
node-addon-api: 8.9.1
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
argparse@1.0.3:
|
||||
dependencies:
|
||||
lodash: 4.18.1
|
||||
sprintf-js: 1.0.3
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asap@2.0.6: {}
|
||||
@@ -3935,10 +4113,14 @@ snapshots:
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-result@2.10.0: {}
|
||||
|
||||
birpc@2.3.0: {}
|
||||
|
||||
bluebird@3.4.7: {}
|
||||
|
||||
body-parser@2.3.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -4082,6 +4264,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cors@2.8.6:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
@@ -4148,6 +4332,8 @@ snapshots:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
dayjs@1.11.21: {}
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -4158,6 +4344,8 @@ snapshots:
|
||||
|
||||
deepmerge-ts@7.1.5: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
defu@6.1.7: {}
|
||||
|
||||
delaunator@5.1.0:
|
||||
@@ -4177,8 +4365,54 @@ snapshots:
|
||||
asap: 2.0.6
|
||||
wrappy: 1.0.2
|
||||
|
||||
dingbat-to-unicode@1.0.1: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.2.0
|
||||
|
||||
dom-serializer@3.1.1:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
entities: 8.0.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domelementtype@3.0.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domhandler@6.0.1:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
|
||||
dompurify@3.4.13:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
domutils@4.0.2:
|
||||
dependencies:
|
||||
dom-serializer: 3.1.1
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
|
||||
dotenv@17.4.2: {}
|
||||
|
||||
duck@0.1.12:
|
||||
dependencies:
|
||||
underscore: 1.13.8
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -4204,8 +4438,12 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
entities@4.2.0: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
entities@8.0.0: {}
|
||||
|
||||
env-paths@3.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
@@ -4538,6 +4776,20 @@ snapshots:
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 7.0.1
|
||||
|
||||
htmlparser2@12.0.0:
|
||||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
domutils: 4.0.2
|
||||
entities: 8.0.0
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -4560,6 +4812,8 @@ snapshots:
|
||||
|
||||
ignore@7.0.6: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
import-fresh@3.2.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -4581,12 +4835,16 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
is-property@1.0.2: {}
|
||||
|
||||
is-what@5.5.0: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
iterare@1.2.1: {}
|
||||
@@ -4613,10 +4871,21 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.2
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
launder@1.7.1:
|
||||
dependencies:
|
||||
dayjs: 1.11.21
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -4624,6 +4893,10 @@ snapshots:
|
||||
|
||||
libphonenumber-js@1.13.10: {}
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
load-esm@1.0.3: {}
|
||||
|
||||
locate-path@6.0.0:
|
||||
@@ -4636,6 +4909,12 @@ snapshots:
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
lop@0.4.2:
|
||||
dependencies:
|
||||
duck: 0.1.12
|
||||
option: 0.2.4
|
||||
underscore: 1.13.8
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
lru-cache@10.2.0: {}
|
||||
@@ -4654,6 +4933,19 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mammoth@1.12.1:
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.8.13
|
||||
argparse: 1.0.3
|
||||
base64-js: 1.5.1
|
||||
bluebird: 3.4.7
|
||||
dingbat-to-unicode: 1.0.1
|
||||
jszip: 3.10.1
|
||||
lop: 0.4.2
|
||||
path-is-absolute: 1.0.1
|
||||
underscore: 1.13.8
|
||||
xmlbuilder: 10.1.1
|
||||
|
||||
mariadb@3.4.5:
|
||||
dependencies:
|
||||
'@types/geojson': 7946.0.16
|
||||
@@ -4767,6 +5059,8 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
option@0.2.4: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -4788,16 +5082,22 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@1.0.2: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-srcset@1.0.2: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
@@ -4861,6 +5161,8 @@ snapshots:
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
proper-lockfile@4.1.2:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -4902,6 +5204,16 @@ snapshots:
|
||||
|
||||
react@19.2.8: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
@@ -4976,6 +5288,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-regex2@5.1.1:
|
||||
@@ -4984,6 +5298,16 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sanitize-html@2.17.6:
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
escape-string-regexp: 4.0.0
|
||||
htmlparser2: 12.0.0
|
||||
is-plain-object: 5.0.0
|
||||
launder: 1.7.1
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.5.26
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
@@ -5015,6 +5339,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -5061,6 +5387,8 @@ snapshots:
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
|
||||
sqlstring@2.3.3: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
@@ -5085,6 +5413,10 @@ snapshots:
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -5212,6 +5544,8 @@ snapshots:
|
||||
|
||||
uint8array-extras@1.5.0: {}
|
||||
|
||||
underscore@1.13.8: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
@@ -5354,6 +5688,8 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
xmlbuilder@10.1.1: {}
|
||||
|
||||
zeptomatch@2.1.0:
|
||||
dependencies:
|
||||
grammex: 3.1.13
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
|
||||
overrides:
|
||||
js-yaml: 5.2.2
|
||||
lodash: 4.18.1
|
||||
|
||||
allowBuilds:
|
||||
'@nestjs/core': true
|
||||
|
||||
Reference in New Issue
Block a user