feat(系统设置): 新增签署设备与文档类型等设置分区并改为索引切换
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 签署设备管理(原型 V10.2 `#setSecDev`)。
|
||||
*
|
||||
* 手写签名板与平板终端共用一张表,列固定为「设备类型 / 设备 / 点位 / 状态 / 操作」。
|
||||
* 操作列按设备类型分流:手写板重连;平板在线自检、离线唤醒(原型 `onclick` 的文案)。
|
||||
* 设备状态是本地演示数据,没有后端接口。
|
||||
*/
|
||||
import type { DeviceKind, SignatureDevice } from '../types'
|
||||
|
||||
defineProps<{
|
||||
devices: SignatureDevice[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
operate: [device: SignatureDevice]
|
||||
}>()
|
||||
|
||||
const kindLabels: Record<DeviceKind, string> = {
|
||||
pad: '手写签名板',
|
||||
tablet: '平板终端',
|
||||
}
|
||||
|
||||
/** 平板在线要带电量(原型「在线 · 电量86%」) */
|
||||
function statusText(device: SignatureDevice) {
|
||||
if (device.status === 'connected') return '已连接'
|
||||
if (device.status === 'disconnected') return '未连接'
|
||||
if (device.status === 'online') {
|
||||
return device.battery === undefined ? '在线' : `在线 · 电量${device.battery}%`
|
||||
}
|
||||
return '离线'
|
||||
}
|
||||
|
||||
function statusClass(device: SignatureDevice) {
|
||||
if (device.kind === 'tablet' && device.status === 'online') return 'status-ipad'
|
||||
if (device.status === 'connected') return 'status-connected'
|
||||
return 'status-idle'
|
||||
}
|
||||
|
||||
/** 手写板只能重连;平板在线自检、离线唤醒 */
|
||||
function actionLabel(device: SignatureDevice) {
|
||||
if (device.kind === 'pad') return '重连'
|
||||
return device.status === 'online' ? '自检' : '唤醒'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="setting-card">
|
||||
<div class="card-heading">
|
||||
<h2>签署设备管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="device-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备类型</th>
|
||||
<th>设备</th>
|
||||
<th>点位</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in devices" :key="device.id">
|
||||
<td>{{ kindLabels[device.kind] }}</td>
|
||||
<td>{{ device.model }}</td>
|
||||
<td>{{ device.location }}</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="statusClass(device)">{{ statusText(device) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="outline-button" @click="emit('operate', device)">
|
||||
{{ actionLabel(device) }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting-card {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.device-table {
|
||||
width: 100%;
|
||||
min-width: 620px;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.device-table th {
|
||||
padding: 9px 12px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
background: #f5f9fa;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.device-table td {
|
||||
padding: 9px 12px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
.device-table tbody tr:hover td {
|
||||
background: #f7fbfc;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* 已连接 → 原型 .tg-done */
|
||||
.status-connected {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
/* 平板在线 → 原型 .tg-ipad */
|
||||
.status-ipad {
|
||||
color: #2b6cb0;
|
||||
background: #e8f1fb;
|
||||
}
|
||||
|
||||
/* 未连接 / 离线 → 原型 .tg-wait */
|
||||
.status-idle {
|
||||
color: var(--info);
|
||||
background: var(--info-l);
|
||||
}
|
||||
|
||||
.outline-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.outline-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文档类型设置(原型 V10.2 `#setDocTypeCard`)。
|
||||
*
|
||||
* 一行标签 + 「新类型名称 / + 添加」,标签右侧的 × 直接删除。
|
||||
* 原型底部提示:文档库 / 新增文书 / 报表筛选的「文档类型」下拉都读这里 —— 目前
|
||||
* 那三处的下拉还是各自的演示词表,本页维护的清单**只在页面内存里**(无后端接口)。
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
types: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: [name: string]
|
||||
remove: [name: string]
|
||||
}>()
|
||||
|
||||
const draft = ref('')
|
||||
|
||||
function submit() {
|
||||
const name = draft.value.trim()
|
||||
if (!name) return
|
||||
emit('add', name)
|
||||
draft.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="setting-card">
|
||||
<div class="card-heading">
|
||||
<h2>文档类型设置</h2>
|
||||
</div>
|
||||
|
||||
<div class="type-row">
|
||||
<span v-for="type in types" :key="type" class="type-tag">
|
||||
{{ type }}
|
||||
<button
|
||||
type="button"
|
||||
class="tag-remove"
|
||||
:aria-label="`删除文档类型 ${type}`"
|
||||
@click="emit('remove', type)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span class="type-add">
|
||||
<input
|
||||
v-model="draft"
|
||||
class="type-input"
|
||||
placeholder="新类型名称"
|
||||
aria-label="新类型名称"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
<button type="button" class="outline-button" @click="submit">+ 添加</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="type-hint">文档库 / 新增文书 / 报表筛选中的「文档类型」下拉即读取此处配置</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting-card {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.type-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 原型 .exam-tag 的配色,字号/内边距按设置页放大一档 */
|
||||
.type-tag {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
color: #2b6cb0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
background: #eef6fb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tag-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.type-add {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.type-input {
|
||||
width: 130px;
|
||||
padding: 5px 8px;
|
||||
color: var(--ink);
|
||||
font-size: 12.5px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.type-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.type-hint {
|
||||
margin: 12px 0 0;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.outline-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.outline-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 签署状态推送(原型 V10.2 `#setSecPush`)。
|
||||
*
|
||||
* 三段结构:已订阅系统标签行(右侧「+ 订阅」)→ 推送日志框 → 底部两个动作按钮。
|
||||
* 推送日志在原型里就是前端拼字符串(`pushSignLog()` 把新记录 unshift 到框顶),
|
||||
* 这里沿用同样的做法,没有后端接口。
|
||||
*/
|
||||
import type { PushLogEntry, PushSubscription } from '../types'
|
||||
|
||||
defineProps<{
|
||||
subscriptions: PushSubscription[]
|
||||
logs: PushLogEntry[]
|
||||
/** 日志框为空时显示的提示文案 */
|
||||
emptyHint: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
subscribe: []
|
||||
pushNow: []
|
||||
saveConfig: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="setting-card">
|
||||
<div class="card-heading">
|
||||
<h2>签署状态推送(对接系统联动)</h2>
|
||||
</div>
|
||||
|
||||
<div class="subscription-row">
|
||||
<span class="subscription-label">已订阅系统:</span>
|
||||
<span v-for="item in subscriptions" :key="item.id" class="subscription-tag">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<button type="button" class="outline-button subscribe-button" @click="emit('subscribe')">
|
||||
+ 订阅
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="push-log">
|
||||
<template v-if="logs.length">
|
||||
<div v-for="(entry, index) in logs" :key="index" :class="`log-${entry.level}`">
|
||||
{{ entry.text }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="log-info">—— 等待签署事件推送 ——</div>
|
||||
<div class="log-hint">{{ emptyHint }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button type="button" class="outline-button" @click="emit('pushNow')">
|
||||
立即推送当前任务状态
|
||||
</button>
|
||||
<button type="button" class="outline-button" @click="emit('saveConfig')">保存配置</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting-card {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.subscription-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.subscription-label {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* 已订阅系统标签沿用原型 .tag.tg-done 的绿 */
|
||||
.subscription-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
color: var(--ok);
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
background: var(--ok-l);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.subscribe-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 原型 .pushlog:等宽小字 + 最高 150px 内滚动,新记录插在最前 */
|
||||
.push-log {
|
||||
max-height: 150px;
|
||||
padding: 8px 10px;
|
||||
overflow-y: auto;
|
||||
color: var(--ink);
|
||||
font-family: Consolas, 'Microsoft YaHei', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.9;
|
||||
word-break: break-all;
|
||||
background: #f4f8fa;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.log-ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.log-warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #8a9aa3;
|
||||
}
|
||||
|
||||
.log-hint {
|
||||
color: var(--mut);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.outline-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.outline-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
</style>
|
||||
@@ -65,6 +65,22 @@ function handleHoursInput(event: Event) {
|
||||
<span class="setting-hint">PDF 原件 + 签名图片</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">短信签署身份校验</span>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active: settings.smsIdentityVerify }"
|
||||
role="switch"
|
||||
:aria-checked="settings.smsIdentityVerify"
|
||||
aria-label="短信签署身份校验"
|
||||
@click="emit('toggle', 'smsIdentityVerify')"
|
||||
>
|
||||
<span aria-hidden="true" />
|
||||
</button>
|
||||
<span class="setting-hint">就诊号 + 姓名 + 证件后四位</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">预留 CA 数字证书</span>
|
||||
<button
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeviceStatus, TabletDevice } from '../types'
|
||||
|
||||
defineProps<{
|
||||
devices: TabletDevice[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reconnect: [device: TabletDevice]
|
||||
}>()
|
||||
|
||||
const statusLabels: Record<DeviceStatus, string> = {
|
||||
connected: '已连接',
|
||||
disconnected: '未连接',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="setting-card">
|
||||
<div class="card-heading">
|
||||
<h2>手写板设备</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="device-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in devices" :key="device.id">
|
||||
<td>{{ device.name }}</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="'status-' + device.status">
|
||||
{{ statusLabels[device.status] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="outline-button" @click="emit('reconnect', device)">
|
||||
重连
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting-card {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.device-table {
|
||||
width: 100%;
|
||||
min-width: 430px;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.device-table th {
|
||||
padding: 9px 12px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
background: #f5f9fa;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.device-table td {
|
||||
padding: 9px 12px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
.device-table tbody tr:hover td {
|
||||
background: #f7fbfc;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--info);
|
||||
background: var(--info-l);
|
||||
}
|
||||
|
||||
.outline-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.outline-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
/**
|
||||
* 系统设置(原型 V10.2 `#sc-set`)。
|
||||
*
|
||||
* 版式照原型:顶部一张「参数设置」卡放**设置项索引**,点索引单选切换,下方单列铺满、
|
||||
* 只显示所选分区(V5.7 加索引 → V5.8 改单选切换 → V6.0 去卡片描述、单列铺满)。
|
||||
*
|
||||
* 分区与接口现状:
|
||||
* - 「菜单设置」是唯一接了真实接口的分区(菜单资源 CRUD,见 MenuResourceSettings),
|
||||
* 本页只负责把它放进索引,**不改它的实现**;无 `system:config:menu` 权限时整项不出现。
|
||||
* - 其余分区(签署参数 / 短信模板 / 签署设备管理 / 签署状态推送 / 第三方系统嵌入 /
|
||||
* 文档类型设置)**暂时都没有后端接口**,全部用页面内演示数据渲染,刷新即回初值。
|
||||
* 这也是本页不引用 `api/management/settings.ts` 的原因。
|
||||
*/
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
|
||||
import DeviceSettingsCard from './components/DeviceSettingsCard.vue'
|
||||
import DocTypeSettingsCard from './components/DocTypeSettingsCard.vue'
|
||||
import EmbedSettingsCard from './components/EmbedSettingsCard.vue'
|
||||
import MenuResourceSettings from './components/MenuResourceSettings.vue'
|
||||
import PushSettingsCard from './components/PushSettingsCard.vue'
|
||||
import SignatureSettingsCard from './components/SignatureSettingsCard.vue'
|
||||
import SmsSettingsCard from './components/SmsSettingsCard.vue'
|
||||
import TabletDevicesCard from './components/TabletDevicesCard.vue'
|
||||
import {
|
||||
defaultSmsChannel,
|
||||
mockDocumentTypes,
|
||||
mockPushSubscriptions,
|
||||
mockSignatureDevices,
|
||||
mockSignatureSettings,
|
||||
mockTabletDevices,
|
||||
pushDefaultForm,
|
||||
pushLogEmptyHint,
|
||||
pushAuthModeOptions,
|
||||
pushEventOptions,
|
||||
pushRetryPolicyOptions,
|
||||
pushSystemTypeOptions,
|
||||
smsChannelOptions,
|
||||
} from './mock'
|
||||
import type { SettingToggleKey, SignatureSettings, SmsChannel, TabletDevice } from './types'
|
||||
import type {
|
||||
PushLogEntry,
|
||||
PushSubscription,
|
||||
PushSubscriptionForm,
|
||||
SettingToggleKey,
|
||||
SignatureDevice,
|
||||
SignatureSettings,
|
||||
SmsChannel,
|
||||
} from './types'
|
||||
|
||||
const EMBED_URL =
|
||||
'https://medisign.hospital.cn/embed/workbench?uid={操作员ID}&pid={患者ID}&visitNo={就诊号}&ts={时间戳}&sign={HMAC签名}'
|
||||
|
||||
const loading = ref(true)
|
||||
const activeTab = ref('general')
|
||||
const loginStore = useLoginStore()
|
||||
const canManageMenus = computed(() => loginStore.hasPermission('system:config:menu'))
|
||||
watch(canManageMenus, (canManage) => {
|
||||
if (!canManage && activeTab.value === 'menus') activeTab.value = 'general'
|
||||
|
||||
type SectionKey = 'sign' | 'sms' | 'device' | 'push' | 'embed' | 'menus' | 'docType'
|
||||
|
||||
const sections = computed<Array<{ key: SectionKey; label: string }>>(() => {
|
||||
const list: Array<{ key: SectionKey; label: string }> = [
|
||||
{ key: 'sign', label: '签署参数' },
|
||||
{ key: 'sms', label: '短信模板' },
|
||||
{ key: 'device', label: '签署设备管理' },
|
||||
{ key: 'push', label: '签署状态推送' },
|
||||
{ key: 'embed', label: '第三方系统嵌入' },
|
||||
]
|
||||
|
||||
if (canManageMenus.value) {
|
||||
list.push({ key: 'menus', label: '菜单设置' })
|
||||
}
|
||||
|
||||
list.push({ key: 'docType', label: '文档类型设置' })
|
||||
return list
|
||||
})
|
||||
|
||||
const activeSection = ref<SectionKey>('sign')
|
||||
watch(canManageMenus, (canManage) => {
|
||||
if (!canManage && activeSection.value === 'menus') activeSection.value = 'sign'
|
||||
})
|
||||
|
||||
/* ---------------- 签署参数 / 短信模板 ---------------- */
|
||||
|
||||
const signatureSettings = reactive<SignatureSettings>({ ...mockSignatureSettings })
|
||||
const smsChannel = ref<SmsChannel>(defaultSmsChannel)
|
||||
const tabletDevices = ref<TabletDevice[]>([])
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true
|
||||
await Promise.resolve()
|
||||
tabletDevices.value = mockTabletDevices.map((device) => ({ ...device }))
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function updateLinkExpireHours(value: number) {
|
||||
signatureSettings.linkExpireHours = value
|
||||
@@ -58,54 +106,301 @@ function sendTestSms() {
|
||||
ElMessage.success('测试短信已发送至 138****5678')
|
||||
}
|
||||
|
||||
function reconnectDevice(device: TabletDevice) {
|
||||
if (device.status === 'connected') {
|
||||
ElMessage.success('已重新连接')
|
||||
/* ---------------- 签署设备管理 ---------------- */
|
||||
|
||||
const devices = ref<SignatureDevice[]>(mockSignatureDevices.map((device) => ({ ...device })))
|
||||
|
||||
function operateDevice(device: SignatureDevice) {
|
||||
if (device.kind === 'pad') {
|
||||
if (device.status === 'connected') {
|
||||
ElMessage.success('已重新连接')
|
||||
} else {
|
||||
ElMessage.info('正在重连…')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info('正在重连…')
|
||||
if (device.status === 'online') {
|
||||
ElMessage.success('已消毒登记 · 固件最新')
|
||||
} else {
|
||||
ElMessage.info('已下发唤醒指令…')
|
||||
}
|
||||
}
|
||||
|
||||
function copyEmbedUrl() {
|
||||
ElMessage.success('原型演示:已复制嵌入 URL')
|
||||
/* ---------------- 签署状态推送 ---------------- */
|
||||
|
||||
const subscriptions = ref<PushSubscription[]>(
|
||||
mockPushSubscriptions.map((subscription) => ({ ...subscription })),
|
||||
)
|
||||
const pushLogs = ref<PushLogEntry[]>([])
|
||||
const subscribeVisible = ref(false)
|
||||
const subscribeForm = ref<PushSubscriptionForm>({ ...pushDefaultForm })
|
||||
|
||||
function formatClock() {
|
||||
const now = new Date()
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||
}
|
||||
|
||||
function rotateKey() {
|
||||
ElMessage.success('原型演示:密钥轮换成功')
|
||||
function openSubscribe() {
|
||||
subscribeForm.value = { ...pushDefaultForm, events: [...pushDefaultForm.events] }
|
||||
subscribeVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(loadSettings)
|
||||
function submitSubscribe() {
|
||||
const name = subscribeForm.value.name.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入系统名称')
|
||||
return
|
||||
}
|
||||
if (!subscribeForm.value.endpoint.trim()) {
|
||||
ElMessage.warning('请输入接收接口地址')
|
||||
return
|
||||
}
|
||||
if (!subscribeForm.value.events.length) {
|
||||
ElMessage.warning('请至少选择一个推送事件')
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions.value.push({
|
||||
id: `sub-${Date.now().toString(36)}`,
|
||||
name,
|
||||
})
|
||||
subscribeVisible.value = false
|
||||
ElMessage.success('订阅已创建,连接测试通过')
|
||||
}
|
||||
|
||||
function pushNow() {
|
||||
const clock = formatClock()
|
||||
pushLogs.value = [
|
||||
{ text: `—— ${clock} 签署状态推送记录 ——`, level: 'info' },
|
||||
{ text: 'POST /api/v2/sign/status/push 任务 T000000', level: 'warn' },
|
||||
{ text: '✓ 岱嘉检查系统已签收:签署完成,机房可执行检查', level: 'ok' },
|
||||
{ text: '✓ 麦迪克斯 PET-CT 已签收:项目关联正常', level: 'ok' },
|
||||
{ text: '✓ 电子病历(EMR)归档通道就绪', level: 'ok' },
|
||||
...pushLogs.value,
|
||||
]
|
||||
ElMessage.success('已向已订阅的对接系统推送签署状态')
|
||||
}
|
||||
|
||||
function savePushConfig() {
|
||||
ElMessage.success('推送配置已保存:轮询间隔 30s / 失败重试 3 次')
|
||||
}
|
||||
|
||||
/* ---------------- 第三方系统嵌入 ---------------- */
|
||||
|
||||
function fallbackCopy(text: string) {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
} catch {
|
||||
// 浏览器既不支持 clipboard API 也禁用了 execCommand 时静默失败,仍给出成功提示
|
||||
}
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
|
||||
async function copyEmbedUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(EMBED_URL)
|
||||
} catch {
|
||||
fallbackCopy(EMBED_URL)
|
||||
}
|
||||
ElMessage.success('嵌入 URL 已复制到剪贴板')
|
||||
}
|
||||
|
||||
const rotateVisible = ref(false)
|
||||
const rotateForm = ref({
|
||||
mode: '双密钥并行 24 小时后失效',
|
||||
bits: '256 位',
|
||||
notify: true,
|
||||
})
|
||||
const rotateModeOptions = ['立即失效旧密钥', '双密钥并行 24 小时后失效']
|
||||
const rotateBitOptions = ['256 位', '512 位']
|
||||
|
||||
function confirmRotate() {
|
||||
rotateVisible.value = false
|
||||
ElMessage.success('密钥轮换成功,旧密钥 24 小时后失效')
|
||||
}
|
||||
|
||||
/* ---------------- 文档类型设置 ---------------- */
|
||||
|
||||
const docTypes = ref<string[]>([...mockDocumentTypes])
|
||||
|
||||
function addDocType(name: string) {
|
||||
if (docTypes.value.includes(name)) {
|
||||
ElMessage.warning('该类型已存在')
|
||||
return
|
||||
}
|
||||
docTypes.value.push(name)
|
||||
ElMessage.success(`文档类型「${name}」已添加`)
|
||||
}
|
||||
|
||||
function removeDocType(name: string) {
|
||||
docTypes.value = docTypes.value.filter((item) => item !== name)
|
||||
ElMessage.success(`文档类型「${name}」已删除`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="settings-page">
|
||||
<el-tabs v-model="activeTab" class="settings-tabs">
|
||||
<el-tab-pane label="系统参数" name="general">
|
||||
<div v-if="loading" class="loading-grid" aria-label="正在加载系统设置">
|
||||
<span v-for="index in 4" :key="index" />
|
||||
</div>
|
||||
<div class="toc-card">
|
||||
<b class="toc-title">参数设置</b>
|
||||
<div class="toc" role="tablist" aria-label="设置项索引">
|
||||
<button
|
||||
v-for="item in sections"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="toc-button"
|
||||
:class="{ on: activeSection === item.key }"
|
||||
role="tab"
|
||||
:aria-selected="activeSection === item.key"
|
||||
@click="activeSection = item.key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="settings-grid">
|
||||
<SignatureSettingsCard
|
||||
:settings="signatureSettings"
|
||||
@update:link-expire-hours="updateLinkExpireHours"
|
||||
@toggle="toggleSetting"
|
||||
/>
|
||||
<SmsSettingsCard
|
||||
:channel="smsChannel"
|
||||
:channel-options="smsChannelOptions"
|
||||
@update:channel="updateSmsChannel"
|
||||
@test="sendTestSms"
|
||||
/>
|
||||
<TabletDevicesCard :devices="tabletDevices" @reconnect="reconnectDevice" />
|
||||
<EmbedSettingsCard @copy="copyEmbedUrl" @rotate-key="rotateKey" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="canManageMenus" label="菜单设置" name="menus">
|
||||
<MenuResourceSettings />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="settings-grid">
|
||||
<SignatureSettingsCard
|
||||
v-if="activeSection === 'sign'"
|
||||
:settings="signatureSettings"
|
||||
@update:link-expire-hours="updateLinkExpireHours"
|
||||
@toggle="toggleSetting"
|
||||
/>
|
||||
|
||||
<SmsSettingsCard
|
||||
v-else-if="activeSection === 'sms'"
|
||||
:channel="smsChannel"
|
||||
:channel-options="smsChannelOptions"
|
||||
@update:channel="updateSmsChannel"
|
||||
@test="sendTestSms"
|
||||
/>
|
||||
|
||||
<DeviceSettingsCard
|
||||
v-else-if="activeSection === 'device'"
|
||||
:devices="devices"
|
||||
@operate="operateDevice"
|
||||
/>
|
||||
|
||||
<PushSettingsCard
|
||||
v-else-if="activeSection === 'push'"
|
||||
:subscriptions="subscriptions"
|
||||
:logs="pushLogs"
|
||||
:empty-hint="pushLogEmptyHint"
|
||||
@subscribe="openSubscribe"
|
||||
@push-now="pushNow"
|
||||
@save-config="savePushConfig"
|
||||
/>
|
||||
|
||||
<EmbedSettingsCard
|
||||
v-else-if="activeSection === 'embed'"
|
||||
@copy="copyEmbedUrl"
|
||||
@rotate-key="rotateVisible = true"
|
||||
/>
|
||||
|
||||
<MenuResourceSettings v-else-if="activeSection === 'menus'" />
|
||||
|
||||
<DocTypeSettingsCard
|
||||
v-else-if="activeSection === 'docType'"
|
||||
:types="docTypes"
|
||||
@add="addDocType"
|
||||
@remove="removeDocType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="subscribeVisible" title="新增推送订阅" width="640px" destroy-on-close>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="系统名称" required>
|
||||
<el-input v-model="subscribeForm.name" placeholder="如:手术麻醉系统 / 体检系统" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统类型">
|
||||
<el-select v-model="subscribeForm.systemType" class="full-width">
|
||||
<el-option
|
||||
v-for="item in pushSystemTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="接收接口地址" required>
|
||||
<el-input v-model="subscribeForm.endpoint" placeholder="https://…/medisign/callback" />
|
||||
</el-form-item>
|
||||
<el-form-item label="鉴权方式">
|
||||
<el-radio-group v-model="subscribeForm.authMode">
|
||||
<el-radio v-for="item in pushAuthModeOptions" :key="item" :value="item">
|
||||
{{ item }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="推送事件">
|
||||
<el-checkbox-group v-model="subscribeForm.events">
|
||||
<el-checkbox v-for="item in pushEventOptions" :key="item" :value="item">
|
||||
{{ item }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="失败重试策略">
|
||||
<el-select v-model="subscribeForm.retryPolicy" class="full-width">
|
||||
<el-option
|
||||
v-for="item in pushRetryPolicyOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="轮询间隔(秒)">
|
||||
<el-input v-model="subscribeForm.pollInterval" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用订阅">
|
||||
<el-switch v-model="subscribeForm.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="subscribeVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitSubscribe">保存并测试连接</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="rotateVisible" title="嵌入接口密钥轮换" width="560px" destroy-on-close>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="密钥 ID">
|
||||
<el-input model-value="KEY-EMBED-2026A" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-input model-value="2026-03-01 09:12" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="最近使用">
|
||||
<el-input model-value="2026-09-22 14:58(IPAD-EMBED)" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="轮换方式">
|
||||
<el-radio-group v-model="rotateForm.mode">
|
||||
<el-radio v-for="item in rotateModeOptions" :key="item" :value="item">
|
||||
{{ item }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密钥位数">
|
||||
<el-select v-model="rotateForm.bits" class="full-width">
|
||||
<el-option v-for="item in rotateBitOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="轮换前通知对接方">
|
||||
<el-switch v-model="rotateForm.notify" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="rotateVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="confirmRotate">确认轮换</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -116,41 +411,66 @@ onMounted(loadSettings)
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-tabs {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-grid,
|
||||
.loading-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-grid span {
|
||||
display: block;
|
||||
height: 240px;
|
||||
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
|
||||
background-size: 400% 100%;
|
||||
/* 原型:外层 .card(padding:12px 16px; margin-bottom:14px)+ 标题与索引同一行 */
|
||||
.toc-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
animation: skeleton-shimmer 1.4s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
.toc-title {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.settings-grid,
|
||||
.loading-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.toc {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 原型索引按钮就是 .btn.gh.sm,选中态换成主色实心 */
|
||||
.toc-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
}
|
||||
|
||||
.toc-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
.toc-button.on {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: var(--brand);
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.toc-button:focus-visible {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 单列铺满:一次只渲染一个分区,所以网格只有一列 */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import type { SignatureSettings, SmsChannel, SmsChannelOption, TabletDevice } from './types'
|
||||
/**
|
||||
* 系统设置页的演示数据(原型 V10.2 · `#sc-set`)。
|
||||
*
|
||||
* 这些分区**都没有后端接口**,数据只活在页面内存里,刷新即回到初值 —— 与原型一致。
|
||||
* 取值口径照抄原型:签署参数 5 项、签署设备 6 台(3 手写签名板 + 3 平板终端)、
|
||||
* 已订阅系统 3 个、文档类型 5 个。
|
||||
*/
|
||||
import type {
|
||||
DocumentType,
|
||||
PushSubscription,
|
||||
PushSubscriptionForm,
|
||||
SignatureDevice,
|
||||
SignatureSettings,
|
||||
SmsChannel,
|
||||
SmsChannelOption,
|
||||
} from './types'
|
||||
|
||||
export const mockSignatureSettings: SignatureSettings = {
|
||||
linkExpireHours: 48,
|
||||
timeoutReminder: true,
|
||||
dualRetention: true,
|
||||
smsIdentityVerify: true,
|
||||
caCertificate: false,
|
||||
}
|
||||
|
||||
@@ -14,20 +30,90 @@ export const smsChannelOptions: SmsChannelOption[] = [
|
||||
|
||||
export const defaultSmsChannel: SmsChannel = 'hospital-primary'
|
||||
|
||||
export const mockTabletDevices: TabletDevice[] = [
|
||||
export const mockSignatureDevices: SignatureDevice[] = [
|
||||
{ id: 'dev-01', kind: 'pad', model: '汉王 GP-1108U', location: '护士站01', status: 'connected' },
|
||||
{ id: 'dev-02', kind: 'pad', model: '汉王 GP-1108U', location: '护士站02', status: 'connected' },
|
||||
{
|
||||
id: 'tablet-01',
|
||||
name: '汉王 GP-1108U(护士站01)',
|
||||
status: 'connected',
|
||||
},
|
||||
{
|
||||
id: 'tablet-02',
|
||||
name: '汉王 GP-1108U(护士站02)',
|
||||
status: 'connected',
|
||||
},
|
||||
{
|
||||
id: 'tablet-03',
|
||||
name: 'Wacom STU-430(放射登记台)',
|
||||
id: 'dev-03',
|
||||
kind: 'pad',
|
||||
model: 'Wacom STU-430',
|
||||
location: '放射登记台',
|
||||
status: 'disconnected',
|
||||
},
|
||||
{
|
||||
id: 'dev-04',
|
||||
kind: 'tablet',
|
||||
model: 'Pad Pro 11″',
|
||||
location: '儿科急诊 · 移动签署车01',
|
||||
status: 'online',
|
||||
battery: 86,
|
||||
},
|
||||
{
|
||||
id: 'dev-05',
|
||||
kind: 'tablet',
|
||||
model: 'Pad Pro 11″',
|
||||
location: '儿科急诊 · 诊间02',
|
||||
status: 'online',
|
||||
battery: 72,
|
||||
},
|
||||
{
|
||||
id: 'dev-06',
|
||||
kind: 'tablet',
|
||||
model: 'Pad Air',
|
||||
location: '核医学科 · PET护士台',
|
||||
status: 'offline',
|
||||
},
|
||||
]
|
||||
|
||||
export const mockPushSubscriptions: PushSubscription[] = [
|
||||
{ id: 'sub-01', name: '岱嘉检查系统' },
|
||||
{ id: 'sub-02', name: '麦迪克斯 PET-CT' },
|
||||
{ id: 'sub-03', name: '电子病历 EMR(归档)' },
|
||||
]
|
||||
|
||||
/** 推送日志为空时的提示(原型 `#pushLogBox` 初值) */
|
||||
export const pushLogEmptyHint =
|
||||
'签署完成后将自动推送「签署状态 + 文档URL」至已订阅系统,未签署/异常任务在岱嘉侧醒目提醒。'
|
||||
|
||||
export const pushSystemTypeOptions = [
|
||||
'HIS',
|
||||
'EMR 电子病历',
|
||||
'LIS',
|
||||
'PACS',
|
||||
'体检系统',
|
||||
'手术麻醉',
|
||||
'第三方归档',
|
||||
]
|
||||
|
||||
export const pushAuthModeOptions = ['HMAC-SHA256 签名', 'OAuth2 客户端凭据', '固定 Token']
|
||||
|
||||
export const pushEventOptions = [
|
||||
'任务发起',
|
||||
'签署完成',
|
||||
'患者拒签',
|
||||
'任务超时',
|
||||
'任务作废',
|
||||
'科室复核签章',
|
||||
]
|
||||
|
||||
export const pushRetryPolicyOptions = ['3 次,间隔 30s', '5 次,间隔 60s', '不重试']
|
||||
|
||||
export const pushDefaultForm: PushSubscriptionForm = {
|
||||
name: '',
|
||||
systemType: 'HIS',
|
||||
endpoint: '',
|
||||
authMode: 'HMAC-SHA256 签名',
|
||||
events: ['签署完成', '患者拒签', '任务超时'],
|
||||
retryPolicy: '3 次,间隔 30s',
|
||||
pollInterval: '30',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
/** 原型 `docTypes` 初值 */
|
||||
export const mockDocumentTypes: DocumentType[] = [
|
||||
'知情同意书',
|
||||
'风险告知书',
|
||||
'入院告知',
|
||||
'检查预约单',
|
||||
'授权委托',
|
||||
]
|
||||
|
||||
@@ -1,37 +1,83 @@
|
||||
export type SettingToggleKey = 'timeoutReminder' | 'dualRetention' | 'caCertificate'
|
||||
/**
|
||||
* 系统设置页(原型 V10.2 · `#sc-set`)的页面模型。
|
||||
*
|
||||
* 除「菜单设置」外,本页各分区目前都**没有后端接口**,页面只用本地演示数据渲染,
|
||||
* 因此这里的类型描述的是「页面要展示什么」,不是接口契约。菜单设置走真实接口,
|
||||
* 其类型在 `@/api/management/types`(PermissionResourceNodeDto 等)。
|
||||
*/
|
||||
|
||||
export type DeviceStatus = 'connected' | 'disconnected'
|
||||
|
||||
export type SmsChannel = 'hospital-primary' | 'third-party-backup'
|
||||
/** 签署参数里可开关的项(对应原型 `.switch` 行) */
|
||||
export type SettingToggleKey =
|
||||
'timeoutReminder' | 'dualRetention' | 'smsIdentityVerify' | 'caCertificate'
|
||||
|
||||
export interface SignatureSettings {
|
||||
/** 线上链接有效期(小时),原型默认 48 */
|
||||
linkExpireHours: number
|
||||
/** 链接超时自动提醒 */
|
||||
timeoutReminder: boolean
|
||||
/** 签名原图双留存:PDF 原件 + 签名图片 */
|
||||
dualRetention: boolean
|
||||
/** 短信签署身份校验:就诊号 + 姓名 + 证件后四位 */
|
||||
smsIdentityVerify: boolean
|
||||
/** 预留 CA 数字证书(扩展位,本期未启用) */
|
||||
caCertificate: boolean
|
||||
}
|
||||
|
||||
export interface TabletDevice {
|
||||
/**
|
||||
* 设备类型决定操作列按钮:
|
||||
* - `pad` 手写签名板 → 重连
|
||||
* - `tablet` 平板终端 → 在线自检 / 离线唤醒
|
||||
*/
|
||||
export type DeviceKind = 'pad' | 'tablet'
|
||||
|
||||
/**
|
||||
* 手写签名板只有「已连接 / 未连接」,平板终端只有「在线 / 离线」;
|
||||
* 原型里它们共用一张表,所以状态放在同一个联合类型里。
|
||||
*/
|
||||
export type DeviceStatus = 'connected' | 'disconnected' | 'online' | 'offline'
|
||||
|
||||
export interface SignatureDevice {
|
||||
id: string
|
||||
name: string
|
||||
kind: DeviceKind
|
||||
/** 设备型号,如「汉王 GP-1108U」 */
|
||||
model: string
|
||||
/** 点位,如「护士站01」「儿科急诊 · 移动签署车01」 */
|
||||
location: string
|
||||
status: DeviceStatus
|
||||
/** 平板终端在线时的电量百分比;手写签名板与离线平板没有该值 */
|
||||
battery?: number
|
||||
}
|
||||
|
||||
export type SmsChannel = 'hospital-primary' | 'third-party-backup'
|
||||
|
||||
export interface SmsChannelOption {
|
||||
value: SmsChannel
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface SettingFormItem {
|
||||
key: string
|
||||
label: string
|
||||
description: string
|
||||
value: string | number | boolean
|
||||
valueType: 'string' | 'number' | 'boolean'
|
||||
/** 签署状态推送的订阅目标(对接系统) */
|
||||
export interface PushSubscription {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface SettingSection {
|
||||
key: string
|
||||
label: string
|
||||
items: SettingFormItem[]
|
||||
/** 新增订阅弹窗的表单值(原型 `openSub()` 的字段集) */
|
||||
export interface PushSubscriptionForm {
|
||||
name: string
|
||||
systemType: string
|
||||
endpoint: string
|
||||
authMode: string
|
||||
events: string[]
|
||||
retryPolicy: string
|
||||
pollInterval: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
/** 推送日志的一条记录;`level` 决定配色(ok 绿 / warn 橙 / info 灰) */
|
||||
export interface PushLogEntry {
|
||||
text: string
|
||||
level: 'ok' | 'warn' | 'info'
|
||||
}
|
||||
|
||||
/** 文档类型设置里的一项;文档库 / 新增文书 / 报表筛选的下拉都读这里 */
|
||||
export type DocumentType = string
|
||||
|
||||
Reference in New Issue
Block a user