feat: 提交
This commit is contained in:
@@ -494,7 +494,7 @@ export async function getTemplateVersionDetail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TemplateVersionAction =
|
export type TemplateVersionAction =
|
||||||
'submit-review' | 'reject' | 'approve' | 'publish' | 'disable' | 'archive'
|
'submit-review' | 'reject' | 'approve' | 'publish' | 'enable' | 'disable' | 'archive'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本工作流动作。
|
* 版本工作流动作。
|
||||||
|
|||||||
@@ -155,6 +155,121 @@ const mockPatients: PatientProfile[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '10000000-0000-4000-8000-000000000001',
|
||||||
|
name: '测试患者001',
|
||||||
|
sex: '男',
|
||||||
|
age: 38,
|
||||||
|
idCard: '测试数据(未提供)',
|
||||||
|
phone: '未提供',
|
||||||
|
visits: [
|
||||||
|
{
|
||||||
|
id: '20000000-0000-4000-8000-000000000001',
|
||||||
|
type: '门诊',
|
||||||
|
visitNo: 'TEST-VISIT-001',
|
||||||
|
department: '心内科',
|
||||||
|
departmentId: '00000000-0000-0000-0001-000000000003',
|
||||||
|
doctorName: '测试医生-心内科',
|
||||||
|
visitDate: '2026-09-29',
|
||||||
|
visitedAt: '2026-09-29T01:10:00Z',
|
||||||
|
campusId: '00000000-0000-0000-0000-000000000001',
|
||||||
|
sourceSystem: 'LOCAL_TEST',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10000000-0000-4000-8000-000000000002',
|
||||||
|
name: '测试患者002',
|
||||||
|
sex: '女',
|
||||||
|
age: 50,
|
||||||
|
idCard: '测试数据(未提供)',
|
||||||
|
phone: '未提供',
|
||||||
|
visits: [
|
||||||
|
{
|
||||||
|
id: '20000000-0000-4000-8000-000000000002',
|
||||||
|
type: '门诊',
|
||||||
|
visitNo: 'TEST-VISIT-002',
|
||||||
|
department: '放射科',
|
||||||
|
departmentId: '00000000-0000-0000-0001-000000000002',
|
||||||
|
doctorName: '测试医生-放射科',
|
||||||
|
visitDate: '2026-09-28',
|
||||||
|
visitedAt: '2026-09-28T02:25:00Z',
|
||||||
|
campusId: '00000000-0000-0000-0000-000000000001',
|
||||||
|
sourceSystem: 'LOCAL_TEST',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10000000-0000-4000-8000-000000000003',
|
||||||
|
name: '测试患者003',
|
||||||
|
sex: '男',
|
||||||
|
age: 25,
|
||||||
|
idCard: '测试数据(未提供)',
|
||||||
|
phone: '未提供',
|
||||||
|
visits: [
|
||||||
|
{
|
||||||
|
id: '20000000-0000-4000-8000-000000000003',
|
||||||
|
type: '门诊',
|
||||||
|
visitNo: 'TEST-VISIT-003',
|
||||||
|
department: '心内科',
|
||||||
|
departmentId: '00000000-0000-0000-0001-000000000003',
|
||||||
|
doctorName: '测试医生-心内科',
|
||||||
|
visitDate: '2026-09-27',
|
||||||
|
visitedAt: '2026-09-27T03:40:00Z',
|
||||||
|
campusId: '00000000-0000-0000-0000-000000000001',
|
||||||
|
sourceSystem: 'LOCAL_TEST',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10000000-0000-4000-8000-000000000004',
|
||||||
|
name: '测试患者004',
|
||||||
|
sex: '女',
|
||||||
|
age: 34,
|
||||||
|
idCard: '测试数据(未提供)',
|
||||||
|
phone: '未提供',
|
||||||
|
visits: [
|
||||||
|
{
|
||||||
|
id: '20000000-0000-4000-8000-000000000004',
|
||||||
|
type: '门诊',
|
||||||
|
visitNo: 'TEST-VISIT-004',
|
||||||
|
department: '放射科',
|
||||||
|
departmentId: '00000000-0000-0000-0001-000000000002',
|
||||||
|
doctorName: '测试医生-放射科',
|
||||||
|
visitDate: '2026-09-26',
|
||||||
|
visitedAt: '2026-09-26T04:55:00Z',
|
||||||
|
campusId: '00000000-0000-0000-0000-000000000001',
|
||||||
|
sourceSystem: 'LOCAL_TEST',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10000000-0000-4000-8000-000000000005',
|
||||||
|
name: '测试患者005',
|
||||||
|
sex: '男',
|
||||||
|
age: 60,
|
||||||
|
idCard: '测试数据(未提供)',
|
||||||
|
phone: '未提供',
|
||||||
|
visits: [
|
||||||
|
{
|
||||||
|
id: '20000000-0000-4000-8000-000000000005',
|
||||||
|
type: '门诊',
|
||||||
|
visitNo: 'TEST-VISIT-005',
|
||||||
|
department: '心内科',
|
||||||
|
departmentId: '00000000-0000-0000-0001-000000000003',
|
||||||
|
doctorName: '测试医生-心内科',
|
||||||
|
visitDate: '2026-09-25',
|
||||||
|
visitedAt: '2026-09-25T06:05:00Z',
|
||||||
|
campusId: '00000000-0000-0000-0000-000000000001',
|
||||||
|
sourceSystem: 'LOCAL_TEST',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const defaultMockRecords: SigningTaskRecord[] = [
|
const defaultMockRecords: SigningTaskRecord[] = [
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ import type { SigningTaskRecord } from '@/api/workbench/types'
|
|||||||
<span>患者 / 授权委托人签名:</span>
|
<span>患者 / 授权委托人签名:</span>
|
||||||
<div class="signature-box">
|
<div class="signature-box">
|
||||||
<img v-if="task.signatureDataUrl" :src="task.signatureDataUrl" alt="患者签名" />
|
<img v-if="task.signatureDataUrl" :src="task.signatureDataUrl" alt="患者签名" />
|
||||||
<span v-else>{{ task.status === 'void' ? '—' : '未签署' }}</span>
|
<span v-else-if="task.status === 'void'">—</span>
|
||||||
|
<span v-else-if="task.status === 'signed' || task.status === 'confirming'">
|
||||||
|
签名图片暂不可显示
|
||||||
|
</span>
|
||||||
|
<span v-else>未签署</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="signature-field">
|
<div class="signature-field">
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ function redirectToLogin() {
|
|||||||
window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`)
|
window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPublicSigningProofEndpoint(url: string | undefined) {
|
||||||
|
return Boolean(
|
||||||
|
url &&
|
||||||
|
/\/v1\/sign-deliveries\/(?:token\/(?:consume|context|signature)|[^/?#]+\/signature)(?:[?#]|$)/.test(
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Request {
|
class Request {
|
||||||
private readonly instance: AxiosInstance
|
private readonly instance: AxiosInstance
|
||||||
|
|
||||||
@@ -45,7 +54,11 @@ class Request {
|
|||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse) => response,
|
(response: AxiosResponse) => response,
|
||||||
(error: unknown) => {
|
(error: unknown) => {
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
if (
|
||||||
|
axios.isAxiosError(error) &&
|
||||||
|
error.response?.status === 401 &&
|
||||||
|
!isPublicSigningProofEndpoint(error.config?.url)
|
||||||
|
) {
|
||||||
clearAuthStorage()
|
clearAuthStorage()
|
||||||
redirectToLogin()
|
redirectToLogin()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -790,6 +790,18 @@ export function buildSigningDocumentHtml(
|
|||||||
renderStructuredFields(root, answers, interactive, signatureActionable)
|
renderStructuredFields(root, answers, interactive, signatureActionable)
|
||||||
renderTokenFields(root, answers, interactive, signatureActionable, claimed)
|
renderTokenFields(root, answers, interactive, signatureActionable, claimed)
|
||||||
|
|
||||||
|
// 已签任务但图片产物暂时取不到时,不要把权威状态渲染成“未签署”。
|
||||||
|
// 签名图本身仍以 SIGNATURE_IMAGE 产物为准;这里仅修正文案,避免误导查看者。
|
||||||
|
if (data.signedAt && !answers[PRIMARY_SIGNATURE_FIELD_ID]) {
|
||||||
|
const pendingLabel = root.querySelector<HTMLElement>(
|
||||||
|
`[${FIELD_ATTR}="${PRIMARY_SIGNATURE_FIELD_ID}"] .docfld-empty`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pendingLabel) {
|
||||||
|
pendingLabel.textContent = '签名图片暂不可显示'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selected = new Set(data.selectedItems ?? [])
|
const selected = new Set(data.selectedItems ?? [])
|
||||||
convertCheckboxTables(root, interactive, selected)
|
convertCheckboxTables(root, interactive, selected)
|
||||||
convertInlineOptions(root, interactive, selected)
|
convertInlineOptions(root, interactive, selected)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
isGeneralOwnership,
|
isGeneralOwnership,
|
||||||
readLibraryStatus,
|
readLibraryStatus,
|
||||||
readStatusTone,
|
readStatusTone,
|
||||||
|
templateWorkflowAction,
|
||||||
|
templateWorkflowLabel,
|
||||||
} from '../library'
|
} from '../library'
|
||||||
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from '../permissions'
|
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from '../permissions'
|
||||||
import type { DocumentTemplate } from '../types'
|
import type { DocumentTemplate } from '../types'
|
||||||
@@ -27,7 +29,22 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const status = computed(() => readLibraryStatus(props.template))
|
const status = computed(() => readLibraryStatus(props.template))
|
||||||
const tone = computed(() => readStatusTone(status.value))
|
const rawStatus = computed(() => props.template.backendStatus?.toUpperCase())
|
||||||
|
const statusLabel = computed(() => {
|
||||||
|
if (rawStatus.value === 'DRAFT') {
|
||||||
|
return props.template.currentVersionId ? '有新版本待发布' : '待发布'
|
||||||
|
}
|
||||||
|
if (rawStatus.value === 'APPROVED') return '待发布'
|
||||||
|
if (rawStatus.value === 'PENDING_REVIEW') return '待审核'
|
||||||
|
if (rawStatus.value === 'REJECTED') return '已驳回'
|
||||||
|
if (rawStatus.value === 'ARCHIVED') return '已归档'
|
||||||
|
return status.value
|
||||||
|
})
|
||||||
|
const tone = computed(() =>
|
||||||
|
['DRAFT', 'APPROVED', 'PENDING_REVIEW', 'REJECTED', 'ARCHIVED'].includes(rawStatus.value ?? '')
|
||||||
|
? 'stopped'
|
||||||
|
: readStatusTone(status.value),
|
||||||
|
)
|
||||||
const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value])
|
const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value])
|
||||||
|
|
||||||
const isGeneral = computed(() => isGeneralOwnership(props.template))
|
const isGeneral = computed(() => isGeneralOwnership(props.template))
|
||||||
@@ -45,18 +62,19 @@ const ownershipLabel = computed(() =>
|
|||||||
const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0)
|
const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只有「启用中」能发起签署;「停用」已下线。
|
* 真实模式必须存在 currentVersionId 才能发起签署;演示模式按「启用中 / 停用」判断。
|
||||||
*
|
* 列表接口不返回正文,正文完整性由发起那一步检查(见 index.vue 的 toSigningTemplate)。
|
||||||
* 这里只看状态,不看正文——真实模式的列表接口不返回正文,按正文判断会让所有
|
|
||||||
* 真实模板的「发起」按钮都消失。缺正文由发起那一步拦下(见 index.vue 的 toSigningTemplate)。
|
|
||||||
*/
|
*/
|
||||||
const canInitiate = computed(() => canInitiateSigning(status.value))
|
const canInitiate = computed(() =>
|
||||||
|
props.template.backendStatus
|
||||||
|
? Boolean(props.template.currentVersionId)
|
||||||
|
: canInitiateSigning(status.value),
|
||||||
|
)
|
||||||
const initiateTitle = computed(() =>
|
const initiateTitle = computed(() =>
|
||||||
canInitiate.value ? '用此文书发起签署' : '仅「启用中」的文书可发起签署',
|
canInitiate.value ? '用此文书发起签署' : '该文书还没有已发布版本,无法发起签署',
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 两档之间都能来回切,所以这个按钮没有「不适用」的状态 */
|
const toggleLabel = computed(() => templateWorkflowLabel(props.template))
|
||||||
const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停用'))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查类型 / 关联项目编码(原型 V10.2 卡片上的两条信息)。
|
* 检查类型 / 关联项目编码(原型 V10.2 卡片上的两条信息)。
|
||||||
@@ -67,14 +85,15 @@ const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停
|
|||||||
const examType = computed(() => props.template.examType?.trim() ?? '')
|
const examType = computed(() => props.template.examType?.trim() ?? '')
|
||||||
const projectCodes = computed(() => (props.template.projectCodes ?? []).filter(Boolean))
|
const projectCodes = computed(() => (props.template.projectCodes ?? []).filter(Boolean))
|
||||||
|
|
||||||
/** 停用/启用走的是后端版本动作:停用 → disable,启用 → publish */
|
const toggleAction = computed<TemplateVersionAction | null>(() =>
|
||||||
const toggleAction = computed<TemplateVersionAction>(() =>
|
templateWorkflowAction(props.template),
|
||||||
status.value === '停用' ? 'publish' : 'disable',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份
|
// 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份
|
||||||
const permissions = DOC_TEMPLATE_PERMISSIONS
|
const permissions = DOC_TEMPLATE_PERMISSIONS
|
||||||
const togglePermission = computed(() => workflowPermission(toggleAction.value))
|
const togglePermission = computed(() =>
|
||||||
|
workflowPermission(toggleAction.value ?? 'disable'),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -83,7 +102,7 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
|
|||||||
<h3 class="tpl-title" :title="template.name">{{ template.name }}</h3>
|
<h3 class="tpl-title" :title="template.name">{{ template.name }}</h3>
|
||||||
<span class="tpl-status" :class="'tone-' + tone">
|
<span class="tpl-status" :class="'tone-' + tone">
|
||||||
<i :style="{ background: dotColor }" aria-hidden="true" />
|
<i :style="{ background: dotColor }" aria-hidden="true" />
|
||||||
{{ status }}
|
{{ statusLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -133,13 +152,16 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
|
|||||||
<span class="ft-spacer" />
|
<span class="ft-spacer" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="toggleAction"
|
||||||
v-permission="togglePermission"
|
v-permission="togglePermission"
|
||||||
type="button"
|
type="button"
|
||||||
class="card-button"
|
class="card-button"
|
||||||
:title="
|
:title="
|
||||||
toggleLabel === '停用'
|
toggleLabel === '停用'
|
||||||
? '停用后各科室不可见、不可发起签署'
|
? '停用后不可发起新的签署'
|
||||||
: '启用后各科室可见并可发起签署'
|
: toggleLabel === '发布'
|
||||||
|
? '发布当前版本后即可发起签署'
|
||||||
|
: '启用后各科室可见并可发起签署'
|
||||||
"
|
"
|
||||||
@click="emit('toggle-status', template)"
|
@click="emit('toggle-status', template)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ import {
|
|||||||
DOCUMENT_LIBRARY_STATUSES,
|
DOCUMENT_LIBRARY_STATUSES,
|
||||||
readLibraryStatus,
|
readLibraryStatus,
|
||||||
resolveOwnershipDepartmentId,
|
resolveOwnershipDepartmentId,
|
||||||
|
templateWorkflowAction,
|
||||||
|
templateWorkflowLabel,
|
||||||
} from './library'
|
} from './library'
|
||||||
import { loadDocumentLibrary, saveDocumentLibrary } from './mock'
|
import { loadDocumentLibrary, saveDocumentLibrary } from './mock'
|
||||||
import { DOC_TEMPLATE_PERMISSIONS } from './permissions'
|
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from './permissions'
|
||||||
import type {
|
import type {
|
||||||
DepartmentTab,
|
DepartmentTab,
|
||||||
DocumentContentForm,
|
DocumentContentForm,
|
||||||
@@ -163,10 +165,8 @@ const departmentTabs = computed<DepartmentTab[]>(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态页签只有两档(启用中 / 停用),外加「全部」。
|
* 状态页签只有两组(启用中 / 停用),外加「全部」。卡片按需显示待发布等具体版本状态;
|
||||||
*
|
* 不按完整后端枚举增加筛选页签。
|
||||||
* 不再有草稿 / 归档 / 需复审:审核链路已废弃,后端状态也会重写。
|
|
||||||
* 也不按后端模板状态枚举做页签——枚举摆到界面上,科室会看到一堆看不懂也点不动的档位。
|
|
||||||
* 计数按**全库**统计(不受科室页签影响),与原型 libCountByStatus 一致。
|
* 计数按**全库**统计(不受科室页签影响),与原型 libCountByStatus 一致。
|
||||||
*/
|
*/
|
||||||
const statusTabs = computed<StatusTab[]>(() => [
|
const statusTabs = computed<StatusTab[]>(() => [
|
||||||
@@ -763,10 +763,11 @@ async function handleContentSubmit(form: DocumentContentForm) {
|
|||||||
* 而 readLibraryStatus 优先读 backendStatus,所以显示口径仍然是准确的。
|
* 而 readLibraryStatus 优先读 backendStatus,所以显示口径仍然是准确的。
|
||||||
*/
|
*/
|
||||||
function toggleLocalStatus(template: DocumentTemplate) {
|
function toggleLocalStatus(template: DocumentTemplate) {
|
||||||
|
const action = templateWorkflowAction(template)
|
||||||
const next: Pick<DocumentRecord, 'status' | 'backendStatus'> =
|
const next: Pick<DocumentRecord, 'status' | 'backendStatus'> =
|
||||||
readLibraryStatus(template) === '停用'
|
action === 'disable'
|
||||||
? { status: 'published', backendStatus: 'PUBLISHED' }
|
? { status: 'archived', backendStatus: 'DISABLED' }
|
||||||
: { status: 'archived', backendStatus: 'DISABLED' }
|
: { status: 'published', backendStatus: 'PUBLISHED' }
|
||||||
|
|
||||||
library.value.forEach((department) =>
|
library.value.forEach((department) =>
|
||||||
department.categories.forEach((category) =>
|
department.categories.forEach((category) =>
|
||||||
@@ -782,27 +783,31 @@ function toggleLocalStatus(template: DocumentTemplate) {
|
|||||||
/**
|
/**
|
||||||
* 停用 / 启用。
|
* 停用 / 启用。
|
||||||
*
|
*
|
||||||
* 真实模式走后端版本动作(停用 → disable,启用 → publish),前端不自行改写 status:
|
* 真实模式走后端版本动作(草稿/停用 → enable,审核通过 → publish,已发布 → disable):
|
||||||
* 「谁在什么时候把这份文书放行到可签署 / 什么时候下线的」必须由后端留痕。
|
* 「谁在什么时候把这份文书放行到可签署 / 什么时候下线的」必须由后端留痕。
|
||||||
*
|
*
|
||||||
* 这是页面上**唯一**的状态动作:状态只有两档,两档之间都能来回切,因此没有
|
* 仅在后端支持的状态上显示动作;审核中、驳回和归档版本不能直接切换。
|
||||||
* 「某些档位不能停用 / 启用」这种分支了。
|
|
||||||
*/
|
*/
|
||||||
async function handleToggleStatus(template: DocumentTemplate) {
|
async function handleToggleStatus(template: DocumentTemplate) {
|
||||||
const status = readLibraryStatus(template)
|
const action = templateWorkflowAction(template)
|
||||||
|
const nextLabel = templateWorkflowLabel(template)
|
||||||
|
|
||||||
if (!canToggleTemplate.value) {
|
if (!action) {
|
||||||
ElMessage.warning('当前账号没有停用 / 启用文书的权限')
|
ElMessage.info('该文书当前状态不可操作')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLabel = status === '停用' ? '启用' : '停用'
|
if (!canWriteDocument(workflowPermission(action))) {
|
||||||
const action = status === '停用' ? 'publish' : 'disable'
|
ElMessage.warning(`当前账号没有${nextLabel}文书的权限`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await ElMessageBox.confirm(
|
const confirmed = await ElMessageBox.confirm(
|
||||||
status === '停用'
|
nextLabel === '发布'
|
||||||
? `确定启用「${template.name}」?启用后各科室可见,并可发起签署。`
|
? `确定发布「${template.name}」${template.version}?发布后该版本可用于发起签署。`
|
||||||
: `确定停用「${template.name}」?停用后不能再发起新的签署,已签署的记录不受影响。`,
|
: nextLabel === '启用'
|
||||||
|
? `确定启用「${template.name}」?启用后各科室可见,并可发起签署。`
|
||||||
|
: `确定停用「${template.name}」?停用后不能再发起新的签署,已签署的记录不受影响。`,
|
||||||
`确认${nextLabel}`,
|
`确认${nextLabel}`,
|
||||||
{ type: 'warning', confirmButtonText: nextLabel, cancelButtonText: '取消' },
|
{ type: 'warning', confirmButtonText: nextLabel, cancelButtonText: '取消' },
|
||||||
)
|
)
|
||||||
@@ -819,13 +824,16 @@ async function handleToggleStatus(template: DocumentTemplate) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!template.versionId) {
|
const versionId = action === 'disable'
|
||||||
|
? template.currentVersionId ?? template.versionId
|
||||||
|
: template.versionId
|
||||||
|
if (!versionId) {
|
||||||
ElMessage.error('该文书没有可操作的版本,请刷新后重试')
|
ElMessage.error('该文书没有可操作的版本,请刷新后重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateTemplateVersionStatus(template.id, template.versionId, action)
|
await updateTemplateVersionStatus(template.id, versionId, action)
|
||||||
ElMessage.success(`已${nextLabel}`)
|
ElMessage.success(`已${nextLabel}`)
|
||||||
await loadLibrary()
|
await loadLibrary()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -997,6 +1005,11 @@ async function openSigningDialog(template: DocumentTemplate) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useMockLibrary && !template.currentVersionId) {
|
||||||
|
ElMessage.info('该文书还没有已发布版本,无法发起签署')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
closePreview()
|
closePreview()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1021,7 +1034,7 @@ function closeSigningDialog() {
|
|||||||
// 闭环:文档库发起签署后跳到工作台并选中该任务,否则任务创建成功但用户看不到后续
|
// 闭环:文档库发起签署后跳到工作台并选中该任务,否则任务创建成功但用户看不到后续
|
||||||
function handleTaskCreated(task: SigningTaskRecord) {
|
function handleTaskCreated(task: SigningTaskRecord) {
|
||||||
closeSigningDialog()
|
closeSigningDialog()
|
||||||
void router.push({ name: 'workbench-signing', query: { taskId: task.id } })
|
void router.push({ path: '/sign/workbench', query: { taskId: task.id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 页面级 ---------------- */
|
/* ---------------- 页面级 ---------------- */
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* 文档库的展示口径:状态、归属、文档类型。
|
* 文档库的展示口径:状态、归属、文档类型。
|
||||||
*
|
*
|
||||||
* 状态只有**两档**:启用中 / 停用(2026-09-28 收敛)。
|
* 状态筛选只有**两组**:启用中 / 停用(2026-09-28 收敛)。卡片可以显示「待发布」等
|
||||||
* 原先沿用原型 V10.2 的 libStatuses(启用中 / 草稿 / 归档 / 需复审 / 停用),
|
* 具体版本状态,但不为这些状态增加独立页签。
|
||||||
* 那套带着「发布 → 审核」的工作流;后端已确认不要审核链路、状态会重写,
|
* 后端仍保留完整版本状态;readLibraryStatus 在这里将它们归入两个筛选组。
|
||||||
* 因此前端先收成两个页签,不再有草稿 / 归档 / 需复审。
|
|
||||||
*
|
*
|
||||||
* 后端模板状态枚举(DRAFT / PENDING_REVIEW / APPROVED / PUBLISHED / DISABLED /
|
* 后端模板状态枚举(DRAFT / PENDING_REVIEW / APPROVED / PUBLISHED / DISABLED /
|
||||||
* ARCHIVED)在接口重写前仍会出现,因此这里保留一次**折叠**:把原始状态压成两档。
|
* ARCHIVED)在接口中保留,因此这里提供**折叠**:把原始状态压成两个筛选组。
|
||||||
* 反向映射(口径 → 后端)只保留「按状态过滤」这一处需要,见 toBackendStatusFilter。
|
* 反向映射(口径 → 后端)只保留「按状态过滤」这一处需要,见 toBackendStatusFilter。
|
||||||
*/
|
*/
|
||||||
import type { DocumentRecord, DocumentStatus } from '@/api/management/types'
|
import type { DocumentRecord, DocumentStatus } from '@/api/management/types'
|
||||||
|
import type { TemplateVersionAction } from '@/api/management/documents'
|
||||||
|
|
||||||
/** 文档库页签与卡片上的状态,取值即显示文案 */
|
/** 文档库页签与卡片上的状态,取值即显示文案 */
|
||||||
export type DocumentLibraryStatus = '启用中' | '停用'
|
export type DocumentLibraryStatus = '启用中' | '停用'
|
||||||
@@ -93,6 +93,52 @@ export function canInitiateSigning(status: DocumentLibraryStatus): boolean {
|
|||||||
return status === '启用中'
|
return status === '启用中'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端版本状态对应的文书操作。DRAFT 和 DISABLED 使用 enable;APPROVED 使用 publish;
|
||||||
|
* 已发布版本使用 disable。审核中、驳回和归档状态不提供切换。
|
||||||
|
*/
|
||||||
|
export function templateWorkflowAction(
|
||||||
|
record: Pick<DocumentRecord, 'status' | 'backendStatus'>,
|
||||||
|
): TemplateVersionAction | null {
|
||||||
|
switch (record.backendStatus?.toUpperCase()) {
|
||||||
|
case 'DRAFT':
|
||||||
|
case 'DISABLED':
|
||||||
|
return 'enable'
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'publish'
|
||||||
|
case 'PUBLISHED':
|
||||||
|
return 'disable'
|
||||||
|
case 'PENDING_REVIEW':
|
||||||
|
case 'REJECTED':
|
||||||
|
case 'ARCHIVED':
|
||||||
|
return null
|
||||||
|
default:
|
||||||
|
// 演示文库没有后端状态,仍按启用中 / 停用两档切换。
|
||||||
|
if (record.backendStatus) return null
|
||||||
|
return readLibraryStatus(record) === '停用' ? 'enable' : 'disable'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateWorkflowLabel(
|
||||||
|
record: Pick<DocumentRecord, 'status' | 'backendStatus'>,
|
||||||
|
): string {
|
||||||
|
switch (record.backendStatus?.toUpperCase()) {
|
||||||
|
case 'DRAFT':
|
||||||
|
case 'APPROVED':
|
||||||
|
return '发布'
|
||||||
|
case 'DISABLED':
|
||||||
|
return '启用'
|
||||||
|
case 'PUBLISHED':
|
||||||
|
return '停用'
|
||||||
|
default:
|
||||||
|
return record.backendStatus
|
||||||
|
? ''
|
||||||
|
: readLibraryStatus(record) === '停用'
|
||||||
|
? '启用'
|
||||||
|
: '停用'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档归属:原型把「通用(全院)」当作科室清单里的一个选项一起勾选,
|
* 文档归属:原型把「通用(全院)」当作科室清单里的一个选项一起勾选,
|
||||||
* 勾中它就等于全院通用,其余科室勾选一并作废(见 docDeptPicks / openDocProps)。
|
* 勾中它就等于全院通用,其余科室勾选一并作废(见 docDeptPicks / openDocProps)。
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const DOC_TEMPLATE_PERMISSIONS = {
|
|||||||
/**
|
/**
|
||||||
* 版本工作流动作的权限码映射。
|
* 版本工作流动作的权限码映射。
|
||||||
*
|
*
|
||||||
* 页面上只剩**一个**状态动作:停用 / 启用(停用 → disable,启用 → publish)。
|
* 页面状态动作按当前版本状态选择:草稿/停用 → enable,审核通过 → publish,已发布 → disable。
|
||||||
* 审核链路(提交审核 / 审核通过 / 驳回)已废弃,页面不再有对应入口;
|
* 审核链路(提交审核 / 审核通过 / 驳回)已废弃,页面不再有对应入口;
|
||||||
* 归档(archive)页面暂不暴露,但 api 层的 deleteTemplate 仍用它承担删除语义。
|
* 归档(archive)页面暂不暴露,但 api 层的 deleteTemplate 仍用它承担删除语义。
|
||||||
*
|
*
|
||||||
@@ -45,6 +45,7 @@ export const TEMPLATE_WORKFLOW_PERMISSIONS: Record<TemplateVersionAction, string
|
|||||||
reject: DOC_TEMPLATE_PERMISSIONS.publish,
|
reject: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||||
approve: DOC_TEMPLATE_PERMISSIONS.publish,
|
approve: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||||
publish: DOC_TEMPLATE_PERMISSIONS.publish,
|
publish: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||||
|
enable: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||||
disable: DOC_TEMPLATE_PERMISSIONS.stop,
|
disable: DOC_TEMPLATE_PERMISSIONS.stop,
|
||||||
archive: DOC_TEMPLATE_PERMISSIONS.stop,
|
archive: DOC_TEMPLATE_PERMISSIONS.stop,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* 文档库页面模型。
|
* 文档库页面模型。
|
||||||
*
|
*
|
||||||
* 状态口径见 ./library:页面只有「启用中 / 停用」两档,
|
* 状态筛选口径见 ./library:页签只有「启用中 / 停用」两组;卡片仍可显示待发布等
|
||||||
* 后端模板状态枚举只在 API 边界出现一次(readLibraryStatus 折叠)。
|
* 具体版本状态。后端模板状态枚举只在 API 边界做归类。
|
||||||
*
|
*
|
||||||
* 「属性」与「正文」是两份表单、两个弹窗:原型 V10.2 刻意把「修改文档」拆成
|
* 「属性」与「正文」是两份表单、两个弹窗:原型 V10.2 刻意把「修改文档」拆成
|
||||||
* 「编辑属性」与「编辑正文」,属性弹窗不碰正文、正文弹窗不碰属性。
|
* 「编辑属性」与「编辑正文」,属性弹窗不碰正文、正文弹窗不碰属性。
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ function downloadMockReport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleView(row: ReportTask) {
|
function handleView(row: ReportTask) {
|
||||||
void router.push({ name: 'workbench-signing', query: { taskId: row.id } })
|
void router.push({ path: '/sign/workbench', query: { taskId: row.id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReportStatusLabel(status: ReportTaskStatus) {
|
function getReportStatusLabel(status: ReportTaskStatus) {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ async function loadOverview() {
|
|||||||
|
|
||||||
function goToSigning(query: Record<string, string> = {}) {
|
function goToSigning(query: Record<string, string> = {}) {
|
||||||
void router.push({
|
void router.push({
|
||||||
name: 'workbench-signing',
|
path: '/sign/workbench',
|
||||||
query: {
|
query: {
|
||||||
campus: selectedCampus.value,
|
campus: selectedCampus.value,
|
||||||
...query,
|
...query,
|
||||||
@@ -133,7 +133,7 @@ function goToSigning(query: Record<string, string> = {}) {
|
|||||||
|
|
||||||
function handleMetricClick(key: HomeMetricKey) {
|
function handleMetricClick(key: HomeMetricKey) {
|
||||||
if (key === 'templates') {
|
if (key === 'templates') {
|
||||||
void router.push({ name: 'management-documents' })
|
void router.push('/doc/library')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ onMounted(() => {
|
|||||||
:rows="rankingRows"
|
:rows="rankingRows"
|
||||||
:period="rankingPeriod"
|
:period="rankingPeriod"
|
||||||
@update:period="handlePeriodChange"
|
@update:period="handlePeriodChange"
|
||||||
@manage="router.push({ name: 'management-documents' })"
|
@manage="router.push('/doc/library')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ function value(value: string | undefined) {
|
|||||||
<div class="signature-box">
|
<div class="signature-box">
|
||||||
<img v-if="record.signatureDataUrl" :src="record.signatureDataUrl" alt="签署人签名" />
|
<img v-if="record.signatureDataUrl" :src="record.signatureDataUrl" alt="签署人签名" />
|
||||||
<span v-else-if="record.status === 'void'">任务已作废,签名不保留</span>
|
<span v-else-if="record.status === 'void'">任务已作废,签名不保留</span>
|
||||||
|
<span v-else-if="record.status === 'signed' || record.status === 'confirming'">
|
||||||
|
签名图片暂不可显示
|
||||||
|
</span>
|
||||||
<span v-else>未采集签名</span>
|
<span v-else>未采集签名</span>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import { getCampuses, getDepartments } from '@/api/management/organization'
|
import { getCampuses, getDepartments } from '@/api/management/organization'
|
||||||
import { downloadSigningArtifact, getSigningArtifacts } from '@/api/workbench/artifacts'
|
import {
|
||||||
|
downloadSigningArtifact,
|
||||||
|
fetchSignatureDataUrl,
|
||||||
|
getSigningArtifacts,
|
||||||
|
} from '@/api/workbench/artifacts'
|
||||||
import { mockCatalogCategories, mockCatalogDepartments } from '@/api/workbench/mock-catalog'
|
import { mockCatalogCategories, mockCatalogDepartments } from '@/api/workbench/mock-catalog'
|
||||||
import {
|
import {
|
||||||
getAllSigningRecords,
|
getAllSigningRecords,
|
||||||
@@ -120,7 +124,8 @@ const canView = computed(() => canRead('sign:record:view'))
|
|||||||
const canExport = computed(
|
const canExport = computed(
|
||||||
() => !loginStore.isReadOnly && canRead('sign:record:export'),
|
() => !loginStore.isReadOnly && canRead('sign:record:export'),
|
||||||
)
|
)
|
||||||
const canDownloadArtifact = computed(() => canRead('sign:artifact:download'))
|
const canViewArtifacts = computed(() => canRead('sign:artifact:view'))
|
||||||
|
const canDownloadArtifact = computed(() => canRead('sign:task:download'))
|
||||||
|
|
||||||
const campusOptions = computed<SigningRecordFilterOption<WorkbenchCampus | 'all'>[]>(() => [
|
const campusOptions = computed<SigningRecordFilterOption<WorkbenchCampus | 'all'>[]>(() => [
|
||||||
{ label: '全部院区', value: 'all' },
|
{ label: '全部院区', value: 'all' },
|
||||||
@@ -276,9 +281,13 @@ async function openRecord(row: SigningRecordRow) {
|
|||||||
|
|
||||||
const requestId = ++detailRequestId
|
const requestId = ++detailRequestId
|
||||||
|
|
||||||
const [detailResult, eventsResult] = await Promise.allSettled([
|
const isSigned = row.status === 'signed' || row.status === 'confirming'
|
||||||
|
const [detailResult, eventsResult, artifactsResult] = await Promise.allSettled([
|
||||||
getSigningRecordDetail(row.id, signingApiOptions.value),
|
getSigningRecordDetail(row.id, signingApiOptions.value),
|
||||||
getSigningRecordEvents(row.id),
|
getSigningRecordEvents(row.id),
|
||||||
|
isSigned && canViewArtifacts.value && canDownloadArtifact.value
|
||||||
|
? getSigningArtifacts(row.id)
|
||||||
|
: Promise.resolve([]),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (requestId !== detailRequestId) {
|
if (requestId !== detailRequestId) {
|
||||||
@@ -288,11 +297,37 @@ async function openRecord(row: SigningRecordRow) {
|
|||||||
if (detailResult.status === 'fulfilled' && detailResult.value) {
|
if (detailResult.status === 'fulfilled' && detailResult.value) {
|
||||||
// 任务接口不返回文档类别与正文,正文靠详情接口补回;
|
// 任务接口不返回文档类别与正文,正文靠详情接口补回;
|
||||||
// 类别沿用列表里带出的那一份,避免被详情里的空值覆盖
|
// 类别沿用列表里带出的那一份,避免被详情里的空值覆盖
|
||||||
detailRecord.value = {
|
let record = {
|
||||||
...row,
|
...row,
|
||||||
...detailResult.value,
|
...detailResult.value,
|
||||||
category: detailResult.value.category || row.category,
|
category: detailResult.value.category || row.category,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 签署图片是独立的 SIGNATURE_IMAGE 产物,不在任务详情接口中。
|
||||||
|
// 记录详情也要像签署工作台一样取回图片,否则已签任务的文书位会误显示“未签署”。
|
||||||
|
if (isSigned && !record.signatureDataUrl && artifactsResult.status === 'fulfilled') {
|
||||||
|
const signatureArtifact = artifactsResult.value.find(
|
||||||
|
(artifact) => artifact.artifactType === 'SIGNATURE_IMAGE',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (signatureArtifact) {
|
||||||
|
const signatureDataUrl = await fetchSignatureDataUrl(signatureArtifact)
|
||||||
|
|
||||||
|
if (requestId !== detailRequestId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatureDataUrl) {
|
||||||
|
record = {
|
||||||
|
...record,
|
||||||
|
signatureDataUrl,
|
||||||
|
fieldAnswers: { ...(record.fieldAnswers ?? {}), signature: signatureDataUrl },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRecord.value = record
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventsResult.status === 'fulfilled') {
|
if (eventsResult.status === 'fulfilled') {
|
||||||
|
|||||||
@@ -270,6 +270,28 @@ defineExpose({ getDocumentElement, locateSignatureSlot })
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="task.status === 'failed' && task.method === 'pad'">
|
||||||
|
<button
|
||||||
|
v-if="allowLocalSigning || allowMouseSigning"
|
||||||
|
type="button"
|
||||||
|
class="action-button"
|
||||||
|
:disabled="padSessionLoading"
|
||||||
|
@click="emit('action', 'pad-sign')"
|
||||||
|
>
|
||||||
|
{{ padSessionLoading ? '正在创建签署会话…' : '↻ 重新签署' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="canSignTask"
|
||||||
|
type="button"
|
||||||
|
class="action-button"
|
||||||
|
:disabled="padSessionLoading"
|
||||||
|
@click="emit('action', 'create-pad-session')"
|
||||||
|
>
|
||||||
|
{{ padSessionLoading ? '正在创建签署会话…' : '↻ 重新创建签署会话' }}
|
||||||
|
</button>
|
||||||
|
<span class="action-hint">上次生成失败;重新签署会创建新的签署会话。</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 待科室确认:患者已签完,此刻要动的是科室那一道章(原型 confirmDept 的动作组)。
|
<!-- 待科室确认:患者已签完,此刻要动的是科室那一道章(原型 confirmDept 的动作组)。
|
||||||
预览与下载放在这里,是因为科室复核时要先看患者签了什么 -->
|
预览与下载放在这里,是因为科室复核时要先看患者签了什么 -->
|
||||||
<template v-else-if="task.status === 'confirming'">
|
<template v-else-if="task.status === 'confirming'">
|
||||||
|
|||||||
@@ -163,8 +163,10 @@ const canConfirmTask = computed(() => canWrite('sign:task:confirm'))
|
|||||||
/** 催办:提醒科室签署员尽快复核 */
|
/** 催办:提醒科室签署员尽快复核 */
|
||||||
const canRemindTask = computed(() => canWrite('sign:task:remind'))
|
const canRemindTask = computed(() => canWrite('sign:task:remind'))
|
||||||
const canViewEvents = computed(() => canRead('sign:task:event:query'))
|
const canViewEvents = computed(() => canRead('sign:task:event:query'))
|
||||||
const canViewArtifacts = computed(() => canRead('sign:artifact:query'))
|
// Keep these permission codes aligned with SignArtifactController: artifact:view
|
||||||
const canDownloadArtifacts = computed(() => canRead('sign:artifact:download'))
|
// lists the index, while task:download authorizes the PNG/PDF download.
|
||||||
|
const canViewArtifacts = computed(() => canRead('sign:artifact:view'))
|
||||||
|
const canDownloadArtifacts = computed(() => canRead('sign:task:download'))
|
||||||
|
|
||||||
/** 需要执行签署权限的任务动作,对应后端 `sign:task:sign`。 */
|
/** 需要执行签署权限的任务动作,对应后端 `sign:task:sign`。 */
|
||||||
const SIGNING_ACTIONS: SigningTaskAction[] = [
|
const SIGNING_ACTIONS: SigningTaskAction[] = [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
|
|
||||||
function resolveBuildId() {
|
function resolveBuildId() {
|
||||||
const configured = process.env.BUILD_ID?.trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
const configured = process.env.BUILD_ID?.trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
||||||
@@ -9,9 +9,13 @@ function resolveBuildId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(() => {
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
const buildId = resolveBuildId()
|
const buildId = resolveBuildId()
|
||||||
const apiProxyTarget = process.env.VITE_API_PROXY_TARGET?.trim() || 'https://ipad.shenynet.com'
|
const apiProxyTarget =
|
||||||
|
process.env.VITE_API_PROXY_TARGET?.trim() ||
|
||||||
|
env.VITE_API_PROXY_TARGET?.trim() ||
|
||||||
|
'http://127.0.0.1:18080'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
@@ -25,6 +29,9 @@ export default defineConfig(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
// The public HTTPS virtual host terminates TLS in Nginx and proxies to this dev server.
|
||||||
|
// Permit only that configured hostname; keep Vite's DNS-rebinding protection enabled.
|
||||||
|
allowedHosts: ['fn.xianyi.de'],
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: apiProxyTarget,
|
target: apiProxyTarget,
|
||||||
|
|||||||
Reference in New Issue
Block a user