feat(settings): implement system settings
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
copy: []
|
||||||
|
rotateKey: []
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="setting-card">
|
||||||
|
<div class="card-heading">
|
||||||
|
<h2>第三方系统嵌入(免登录接口页)</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
签署工作台可作为 iframe 嵌入 HIS / LIS / 体检等第三方系统,URL 携带签名参数免登录直达:
|
||||||
|
</p>
|
||||||
|
<div class="code-block embed-url">
|
||||||
|
https://medisign.hospital.cn<strong>/embed/workbench</strong>?uid=<strong>{操作员ID}</strong>&pid=<strong>{患者ID}</strong>&visitNo=<strong>{就诊号}</strong>&ts=<strong>{时间戳}</strong>&sign=<strong
|
||||||
|
>{HMAC签名}</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="description rule-description">
|
||||||
|
生成规则:HMAC-SHA256(密钥, uid+pid+visitNo+ts),有效期 ≤ 5 分钟,防篡改防重放。
|
||||||
|
</p>
|
||||||
|
<div class="button-row">
|
||||||
|
<button type="button" class="outline-button" @click="emit('copy')">复制嵌入 URL</button>
|
||||||
|
<button type="button" class="outline-button" @click="emit('rotateKey')">密钥轮换</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Consolas, 'Microsoft YaHei', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f4f8fa;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block strong {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-description {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,192 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SettingToggleKey, SignatureSettings } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
settings: SignatureSettings
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:linkExpireHours': [value: number]
|
||||||
|
toggle: [key: SettingToggleKey]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleHoursInput(event: Event) {
|
||||||
|
emit('update:linkExpireHours', Number((event.target as HTMLInputElement).value))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="setting-card">
|
||||||
|
<div class="card-heading">
|
||||||
|
<h2>签署参数</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">线上链接有效期</span>
|
||||||
|
<input
|
||||||
|
:value="settings.linkExpireHours"
|
||||||
|
class="hours-input"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
aria-label="线上链接有效期"
|
||||||
|
@input="handleHoursInput"
|
||||||
|
/>
|
||||||
|
<span class="setting-hint">小时</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">链接超时自动提醒</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-button"
|
||||||
|
:class="{ active: settings.timeoutReminder }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="settings.timeoutReminder"
|
||||||
|
aria-label="链接超时自动提醒"
|
||||||
|
@click="emit('toggle', 'timeoutReminder')"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">签名原图双留存</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-button"
|
||||||
|
:class="{ active: settings.dualRetention }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="settings.dualRetention"
|
||||||
|
aria-label="签名原图双留存"
|
||||||
|
@click="emit('toggle', 'dualRetention')"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<span class="setting-hint">PDF 原件 + 签名图片</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">预留 CA 数字证书</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-button"
|
||||||
|
:class="{ active: settings.caCertificate }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="settings.caCertificate"
|
||||||
|
aria-label="预留 CA 数字证书"
|
||||||
|
@click="emit('toggle', 'caCertificate')"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<span class="setting-hint">扩展位,本期未启用</span>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
flex: 0 0 150px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-input {
|
||||||
|
width: 80px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-hint {
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
background: #cbd8de;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: left 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button.active {
|
||||||
|
background: var(--ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button.active::after {
|
||||||
|
left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:focus-visible {
|
||||||
|
outline: 2px solid var(--brand);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.setting-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SmsChannel, SmsChannelOption } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
channel: SmsChannel
|
||||||
|
channelOptions: SmsChannelOption[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:channel': [value: SmsChannel]
|
||||||
|
test: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleChannelChange(event: Event) {
|
||||||
|
emit('update:channel', (event.target as HTMLSelectElement).value as SmsChannel)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="setting-card">
|
||||||
|
<div class="card-heading">
|
||||||
|
<h2>短信模板</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-block sms-template">
|
||||||
|
【xx医院】尊敬的<strong>{患者姓名}</strong>,您有一份《<strong>{文档名称}</strong>》待签署,请点击
|
||||||
|
<strong>{签署链接}</strong> 完成签署,链接 {有效期} 小时内有效。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<label class="channel-row">
|
||||||
|
<span>短信通道</span>
|
||||||
|
<select :value="channel" aria-label="选择短信通道" @change="handleChannelChange">
|
||||||
|
<option v-for="option in channelOptions" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" class="outline-button" @click="emit('test')">发送测试短信</button>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Consolas, 'Microsoft YaHei', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f4f8fa;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block strong {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 12px 0;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row > span {
|
||||||
|
flex: 0 0 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row select {
|
||||||
|
width: 200px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-button {
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-button:hover {
|
||||||
|
background: var(--brand-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.channel-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row > span {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<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,12 +1,136 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PagePlaceholder from '@/components/PagePlaceholder.vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
import EmbedSettingsCard from './components/EmbedSettingsCard.vue'
|
||||||
|
import SignatureSettingsCard from './components/SignatureSettingsCard.vue'
|
||||||
|
import SmsSettingsCard from './components/SmsSettingsCard.vue'
|
||||||
|
import TabletDevicesCard from './components/TabletDevicesCard.vue'
|
||||||
|
import {
|
||||||
|
defaultSmsChannel,
|
||||||
|
mockSignatureSettings,
|
||||||
|
mockTabletDevices,
|
||||||
|
smsChannelOptions,
|
||||||
|
} from './mock'
|
||||||
|
import type { SettingToggleKey, SignatureSettings, SmsChannel, TabletDevice } from './types'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSetting(key: SettingToggleKey) {
|
||||||
|
signatureSettings[key] = !signatureSettings[key]
|
||||||
|
|
||||||
|
if (key === 'caCertificate' && signatureSettings.caCertificate) {
|
||||||
|
ElMessage.info('已开启 CA 扩展位(当前版本未启用)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.info('设置已更新')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSmsChannel(value: SmsChannel) {
|
||||||
|
smsChannel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTestSms() {
|
||||||
|
ElMessage.success('测试短信已发送至 138****5678')
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnectDevice(device: TabletDevice) {
|
||||||
|
if (device.status === 'connected') {
|
||||||
|
ElMessage.success('已重新连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.info('正在重连…')
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyEmbedUrl() {
|
||||||
|
ElMessage.success('原型演示:已复制嵌入 URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateKey() {
|
||||||
|
ElMessage.success('原型演示:密钥轮换成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadSettings)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PagePlaceholder
|
<section class="settings-page">
|
||||||
group="管理"
|
<div v-if="loading" class="loading-grid" aria-label="正在加载系统设置">
|
||||||
title="系统设置"
|
<span v-for="index in 4" :key="index" />
|
||||||
icon="⚙"
|
</div>
|
||||||
description="系统设置页面占位,待根据 Web 端需求实现。"
|
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.settings-grid,
|
||||||
|
.loading-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
33
clinical-web/src/views/management/settings/mock.ts
Normal file
33
clinical-web/src/views/management/settings/mock.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { SignatureSettings, SmsChannel, SmsChannelOption, TabletDevice } from './types'
|
||||||
|
|
||||||
|
export const mockSignatureSettings: SignatureSettings = {
|
||||||
|
linkExpireHours: 48,
|
||||||
|
timeoutReminder: true,
|
||||||
|
dualRetention: true,
|
||||||
|
caCertificate: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const smsChannelOptions: SmsChannelOption[] = [
|
||||||
|
{ value: 'hospital-primary', label: '医院短信网关(主)' },
|
||||||
|
{ value: 'third-party-backup', label: '第三方短信通道(备)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const defaultSmsChannel: SmsChannel = 'hospital-primary'
|
||||||
|
|
||||||
|
export const mockTabletDevices: TabletDevice[] = [
|
||||||
|
{
|
||||||
|
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(放射登记台)',
|
||||||
|
status: 'disconnected',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,10 +1,32 @@
|
|||||||
export type SettingValue = string | number | boolean
|
export type SettingToggleKey = 'timeoutReminder' | 'dualRetention' | 'caCertificate'
|
||||||
|
|
||||||
|
export type DeviceStatus = 'connected' | 'disconnected'
|
||||||
|
|
||||||
|
export type SmsChannel = 'hospital-primary' | 'third-party-backup'
|
||||||
|
|
||||||
|
export interface SignatureSettings {
|
||||||
|
linkExpireHours: number
|
||||||
|
timeoutReminder: boolean
|
||||||
|
dualRetention: boolean
|
||||||
|
caCertificate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabletDevice {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: DeviceStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsChannelOption {
|
||||||
|
value: SmsChannel
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingFormItem {
|
export interface SettingFormItem {
|
||||||
key: string
|
key: string
|
||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
value: SettingValue
|
value: string | number | boolean
|
||||||
valueType: 'string' | 'number' | 'boolean'
|
valueType: 'string' | 'number' | 'boolean'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user