提交
This commit is contained in:
1 parent
15ed25e33b
commit
dc31968204
143 files changed
+15823
-9989
No files matched your search
@@ -40,3 +40,22 @@
|
||||
|
||||
- **WHEN** 在 `app/api/` 增加新的路由模块
|
||||
- **THEN** 该模块 SHALL 在 `app/api/__init__.py` 与 `app/main.py` 中完成注册,并符合现有认证与租户上下文注入模式
|
||||
|
||||
### Requirement: 细胞存储样本跨表关联键
|
||||
|
||||
细胞存储场景下,将协议样本行(`biz_protocol_sample`)与下游业务(样本接收 `biz_sample_reception`、质检工单 `biz_qc_work_order` 等)进行**业务匹配**时,关联键 SHALL 仅为 `tenant_no` + `sample_code`(租户内样本编码唯一且不变;接收/工单表须持久化 `sample_code` 快照)。**不得**使用 `collection_protocol_id` 或 `protocol_sample_id` 作为查询、列表排除、详情回显的关联依据(协议单可换号/转科研、编辑保存会重建样本行 `id`,但样本编码不变)。
|
||||
|
||||
#### Scenario: 协议单查看弹窗展示样本接收
|
||||
|
||||
- **WHEN** 用户打开采集协议单详情,且租户内某 `sample_code` 已登记接收
|
||||
- **THEN** 详情接口 SHALL 仅按样本编码返回对应 `sampleReception`(及同规则的 `qcInfo`),与当前协议单 ID、样本行 `id` 是否变更无关
|
||||
|
||||
#### Scenario: 待接收列表排除已接收样本
|
||||
|
||||
- **WHEN** 租户内某 `sample_code` 已存在有效接收记录(`del_flag=0`)
|
||||
- **THEN** 该编码的协议样本 SHALL NOT 出现在待接收列表
|
||||
|
||||
#### Scenario: 登记接收防重复
|
||||
|
||||
- **WHEN** 用户对某协议样本提交接收登记
|
||||
- **THEN** 服务端 SHALL 按 `tenant_no` + `sample_code` 校验是否已接收,已存在则拒绝重复登记
|
||||
@@ -263,22 +263,52 @@ Web 管理端弹窗 SHALL 遵循规范:表单 `label-position="top"`;遮罩
|
||||
|
||||
### Requirement: 附件上传卡片化布局
|
||||
|
||||
`web-admin` 中弹窗表单的附件上传区域 SHALL 使用**卡片化布局**:上传触发器与已上传附件在同一行 flex 容器中排列,上传卡片作为第一个元素始终可见。具体规则:
|
||||
`web-admin` 弹窗表单中的附件上传与展示 SHALL 采用**卡片化布局**,实现与交互以 `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` 中「协议附件」为权威参考;储存证等业务弹窗(如 `web-admin/src/views/cell-storage/storage-cert/index.vue`)SHALL 复用同一套 DOM 结构、类名与样式规则,仅调整 `el-form-item` 的 `label` 与上传 `folder` 等业务参数。
|
||||
|
||||
1. **上传卡片样式**:上传触发器 SHALL 渲染为与附件卡片等尺寸的卡片(宽 120px、高 96px),使用虚线边框(`border: 1px dashed #a7f3d0`)、浅绿背景(`background: #f0fdf4`),居中显示 `Plus` 图标(size 28)与"上传附件"文字(12px `#10b981`)。hover 时边框变实线(`border-color: #10b981`)、背景加深(`background: #ecfdf5`)。
|
||||
2. **同行排列**:上传卡片与已上传附件卡片 SHALL 放在同一个 `display: flex; flex-wrap: wrap; gap: 12px` 容器内,上传卡片为第一个子元素。容器 SHALL 始终可见(不受附件数量影响)。
|
||||
3. **附件卡片样式**:已上传附件卡片 SHALL 为 120px 宽、96px 预览区高度,带实线边框(`border: 1px solid #e2e8f0`)。图片文件显示缩略图(`object-fit: cover`),视频文件显示紫色视频图标,其他文件显示蓝色文档图标。卡片底部显示文件名(11px,最多 2 行省略)与操作按钮(下载、删除,居中排列)。
|
||||
4. **上传中状态**:正在上传的附件卡片 SHALL 设置 `opacity: 0.6` 且 `pointer-events: none`,预览区显示 `Loading` 图标。
|
||||
5. **提示文案**:格式说明文字(如"支持图片、视频、PDF...,单文件不超过 20MB")SHALL 显示在卡片行下方,作为独立的 `11px` 灰色小字(`color: #94a3b8`)。
|
||||
6. **颜色约束**:上传卡片与附件卡片 SHALL 使用 Fresh Mint 色板(`#10b981`、`#a7f3d0`、`#f0fdf4`、`#ecfdf5`),SHALL NOT 使用小程序品牌色 `#0a9f6d`。
|
||||
#### 布局与视觉
|
||||
|
||||
#### 标准写法示例
|
||||
1. **结构**:`el-form-item`(建议 `class="form-col-span-2"` 占满弹窗栅格宽)→ `.attachment-upload` → `.attachment-list`(flex 行)→ 上传卡片 + 若干 `.attachment-item`;`.upload-hint` 在 flex 行**下方**独立一行。
|
||||
2. **上传卡片**:宽 **140px**、高 **96px**;`border: 1px dashed #a7f3d0`;`background: #f0fdf4`;`border-radius: 8px`;居中 `Plus`(size 28,`#10b981`)+ 文案「上传附件」(`.upload-card-text`,12px `#10b981` font-weight 500)。hover:`border-color: #10b981`、`border-style: solid`、`background: #ecfdf5`。
|
||||
3. **附件卡片**:宽 **140px**;预览区高 **96px**;`border: 1px solid #e2e8f0`;`border-radius: 8px`;hover:`border-color: #10b981`、`box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1)`。
|
||||
4. **预览区**(`.attachment-preview`):背景 `#f8fafc`;图片已上传且 `fileUrl` 存在时显示缩略图(`object-fit: cover`),可点击预览;图片上传中无 URL 时显示 `Loading`;视频显示 `.attachment-preview__icon--video`(`#ede9fe` / `#7c3aed` + `VideoPlay`);其他文件显示 `.attachment-preview__icon--doc`(`#e0f2fe` / `#0284c7` + `Document`)。
|
||||
5. **卡片底部**(`.attachment-footer`):文件名 `.attachment-name`(11px `#334155`,最多 2 行省略,`title` 为完整文件名);`.attachment-actions` 居中排列 **下载**(`Download`)、**删除**(`Delete`,`.action-btn--danger` hover 红色)。
|
||||
6. **上传中**:`.attachment-item.is-uploading` → `opacity: 0.6`、`pointer-events: none`。
|
||||
7. **提示**:`.upload-hint` — 11px `#94a3b8`,`margin-top: 8px`;默认文案:「支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB」。
|
||||
8. **色板**:SHALL 使用 Fresh Mint(`#10b981`、`#a7f3d0`、`#f0fdf4`、`#ecfdf5`);SHALL NOT 使用小程序主色 `#0a9f6d`。
|
||||
9. **禁止**:SHALL NOT 使用 `el-upload` 的 `list-type="picture-card"`、默认 `file-list` 展示,或单独一行纯文字「选择文件」按钮替代上传卡片。
|
||||
|
||||
```html
|
||||
#### `el-upload` 配置
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| `:auto-upload` | `false` |
|
||||
| `:show-file-list` | `false` |
|
||||
| `:multiple` | `true` |
|
||||
| `accept` | `image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar` |
|
||||
| `@change` | 选中后立即走上传逻辑(见下) |
|
||||
|
||||
上传卡片 SHALL 作为 `.attachment-list` 的**第一个子元素**,且始终渲染(不因已有附件数量而隐藏)。
|
||||
|
||||
#### 数据模型与上传逻辑
|
||||
|
||||
附件列表 SHALL 为响应式数组,元素至少包含:`id`、`fileName`、`fileUrl`、`fileType`(可选)、`_uploading`(可选布尔)。
|
||||
|
||||
1. **选择文件后**:校验单文件 ≤ **20MB**;超限则 `ElMessage.error` 并 `clearFiles()`。
|
||||
2. **立即上传**:向数组 push 占位项(`_uploading: true`、`fileUrl: ''`);`FormData` 调用 `fetchUploadAttachment`(`@/api/business`),`fd.append('folder', '<业务目录>')`(如 `protocol`、`storage-cert`、`protocol-payment`);成功后回填 `fileUrl` 等字段并 `_uploading: false`;失败则移除该项并提示。
|
||||
3. **收尾**:`nextTick` 后 `uploadRef.clearFiles()`,避免同一文件无法重复选择。
|
||||
4. **删除**:从数组 `splice` 移除,不单独调删除接口(除非产品另有要求)。
|
||||
5. **下载**:使用 `@/utils/download-attachment` 的 `downloadAttachmentFile(file, { fullFileUrl })`;`fullFileUrl` 需拼接 `VITE_MINIO_BASE_URL`(与参考页一致)。
|
||||
6. **预览**:图片/视频已上传项点击预览区,打开独立 `el-dialog`(宽 800px,`destroy-on-close`);图片用 `<img>`,视频用 `<video controls>`。
|
||||
7. **提交**:若存在 `_uploading === true` 的项,SHALL 阻止提交并提示「附件上传中,请稍候」;业务字段若存逗号分隔 URL,提交前 SHALL 从数组中过滤 `fileUrl` 非空且非上传中的项拼接。
|
||||
|
||||
缴费明细等嵌套场景可使用 `.upload-card--sm`(宽 96px、高 76px),其余规则不变。
|
||||
|
||||
#### 标准模板(Vue)
|
||||
|
||||
```vue
|
||||
<el-form-item label="协议附件" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<!-- 上传卡片始终在第一个 -->
|
||||
<el-upload
|
||||
ref="attachmentUploadRef"
|
||||
:auto-upload="false"
|
||||
@@ -292,17 +322,46 @@ Web 管理端弹窗 SHALL 遵循规范:表单 `label-position="top"`;遮罩
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<!-- 已上传附件卡片 -->
|
||||
<div v-for="(file, index) in formData.attachments" :key="file.id || index"
|
||||
class="attachment-item" :class="{ 'is-uploading': file._uploading }">
|
||||
<div class="attachment-preview">
|
||||
<!-- 图片缩略图 / 视频图标 / 文档图标 -->
|
||||
<div
|
||||
v-for="(file, index) in attachments"
|
||||
:key="file.id || index"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined"
|
||||
>
|
||||
<template v-if="isImageFile(file) && file.fileUrl">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else-if="isImageFile(file) && !file.fileUrl">
|
||||
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
|
||||
</template>
|
||||
<template v-else-if="isVideoFile(file)">
|
||||
<div class="attachment-preview__icon attachment-preview__icon--video">
|
||||
<el-icon :size="28"><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name">{{ file.fileName }}</span>
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<div class="attachment-actions">
|
||||
<el-icon class="action-btn" title="下载" @click="handleDownload(file)"><Download /></el-icon>
|
||||
<el-icon class="action-btn action-btn--danger" title="删除" @click="removeAttachment(index)"><Delete /></el-icon>
|
||||
<el-icon class="action-btn" title="下载" @click="handleDownloadAttachment(file)">
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-btn action-btn--danger"
|
||||
title="删除"
|
||||
@click="removeAttachment(index)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,135 +371,86 @@ Web 管理端弹窗 SHALL 遵循规范:表单 `label-position="top"`;遮罩
|
||||
</el-form-item>
|
||||
```
|
||||
|
||||
> 说明:`attachments` 为业务侧附件数组变量名;图标 `Plus`、`Loading`、`VideoPlay`、`Document`、`Download`、`Delete` SHALL 在 `<script setup>` 中从 `@element-plus/icons-vue` 显式 import。
|
||||
|
||||
#### 标准样式(SCSS,弹窗 `scoped` 内)
|
||||
|
||||
完整类名与尺寸 SHALL 与参考页一致,核心片段:
|
||||
|
||||
```scss
|
||||
.attachment-upload { width: 100%; }
|
||||
.attachment-list { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
border-style: solid;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
width: 140px; height: 96px;
|
||||
border: 1px dashed #a7f3d0; border-radius: 8px; background: #f0fdf4;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 6px; cursor: pointer;
|
||||
&:hover { border-color: #10b981; border-style: solid; background: #ecfdf5; }
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card-text { font-size: 12px; color: #10b981; font-weight: 500; line-height: 1; }
|
||||
.upload-hint { display: block; font-size: 11px; color: #94a3b8; margin-top: 8px; line-height: 1; }
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
width: 140px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; overflow: hidden;
|
||||
&.is-uploading { opacity: 0.6; pointer-events: none; }
|
||||
&:hover { border-color: #10b981; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1); }
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
width: 100%; height: 96px; display: flex; align-items: center; justify-content: center;
|
||||
background: #f8fafc; overflow: hidden; cursor: pointer;
|
||||
img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
padding: 6px 8px;
|
||||
position: relative;
|
||||
.attachment-preview__icon {
|
||||
width: 48px; height: 48px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
&--video { background: #ede9fe; color: #7c3aed; }
|
||||
&--doc { background: #e0f2fe; color: #0284c7; }
|
||||
}
|
||||
|
||||
.attachment-footer { display: flex; flex-direction: column; padding: 6px 8px; }
|
||||
.attachment-name {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||
overflow: hidden; font-size: 11px; color: #334155; line-height: 1.4; word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
display: flex; justify-content: center; gap: 8px; margin-top: 4px;
|
||||
}
|
||||
.action-btn {
|
||||
width: 22px; height: 22px; border-radius: 4px; background: #f1f5f9; color: #64748b;
|
||||
cursor: pointer;
|
||||
&:hover { background: #10b981; color: #fff; }
|
||||
&--danger:hover { background: #ef4444; color: #fff; }
|
||||
}
|
||||
```
|
||||
|
||||
只读查看态 SHALL 使用 `web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue`(无上传卡片、无删除,可保留下载/预览)。
|
||||
|
||||
#### Scenario: 新增弹窗包含附件上传
|
||||
|
||||
- **WHEN** 在弹窗表单中新增附件上传功能
|
||||
- **THEN** 上传触发器 SHALL 渲染为卡片样式(120×96px,虚线边框),与附件卡片在同一 flex 行中排列
|
||||
- **THEN** 容器 SHALL 始终可见(上传卡片始终显示),提示文案在卡片行下方
|
||||
- **WHEN** 在弹窗表单中新增附件上传
|
||||
- **THEN** SHALL 使用上述 DOM 结构、类名与 140×96 卡片尺寸
|
||||
- **THEN** 上传卡片 SHALL 位于 `.attachment-list` 第一项且始终可见
|
||||
- **THEN** `.upload-hint` SHALL 位于卡片行下方
|
||||
|
||||
#### Scenario: 附件卡片预览
|
||||
#### Scenario: 附件卡片预览与操作
|
||||
|
||||
- **WHEN** 用户上传图片文件
|
||||
- **THEN** 附件卡片预览区 SHALL 显示图片缩略图(`object-fit: cover`),点击可预览大图
|
||||
- **WHEN** 用户上传视频文件
|
||||
- **THEN** 预览区 SHALL 显示紫色视频图标(`background: #ede9fe; color: #7c3aed`)
|
||||
- **WHEN** 用户上传其他类型文件
|
||||
- **THEN** 预览区 SHALL 显示蓝色文档图标(`background: #e0f2fe; color: #0284c7`)
|
||||
- **WHEN** 附件为已上传图片
|
||||
- **THEN** 预览区 SHALL 显示缩略图,点击 SHALL 打开预览弹窗
|
||||
- **WHEN** 附件为视频或其他文档
|
||||
- **THEN** 预览区 SHALL 分别显示视频/文档图标(见布局与视觉 §4)
|
||||
- **WHEN** 用户点击下载或删除
|
||||
- **THEN** SHALL 分别调用 `downloadAttachmentFile` 与数组移除逻辑
|
||||
|
||||
#### Scenario: 附件上传中状态
|
||||
|
||||
- **WHEN** 文件正在上传到服务器
|
||||
- **THEN** 对应附件卡片 SHALL 显示为半透明(`opacity: 0.6`)且不可操作(`pointer-events: none`),预览区显示 Loading 图标
|
||||
- **WHEN** 文件正在上传
|
||||
- **THEN** 对应卡片 SHALL 为 `is-uploading`(半透明、不可点),预览区显示 `Loading`
|
||||
- **WHEN** 用户提交表单且仍有 `_uploading` 项
|
||||
- **THEN** SHALL 阻止提交并提示等待上传完成
|
||||
|
||||
#### Scenario: Code review 附件上传组件
|
||||
|
||||
- **WHEN** PR 包含新增或修改的附件上传区域
|
||||
- **THEN** 审查者 SHALL 确认使用卡片化布局(上传卡片 + 附件卡片同行),SHALL NOT 使用独立的上传按钮行或 Element Plus 默认文件列表
|
||||
#### Scenario: Code review 附件上传
|
||||
|
||||
- **WHEN** PR 新增或修改附件上传 UI
|
||||
- **THEN** 审查者 SHALL 对照 `ProtocolFormDialog.vue` 确认卡片化布局与立即上传逻辑
|
||||
- **THEN** SHALL 拒绝 `picture-card`、`show-file-list` 默认列表、与规范不一致的尺寸/色值
|
||||
### Requirement: 按钮须携带图标
|
||||
|
||||
`web-admin` 中所有带文字标签的 `el-button`(或 `ElButton`)SHALL 在文字前方包含 `<el-icon>` 子元素,使用 `@element-plus/icons-vue` 提供的图标组件;SHALL NOT 出现仅有文字而无图标的按钮。图标的引入方式为在文件 `<script setup>` 中显式 `import` 所需图标组件(`@element-plus/icons-vue` 不属于 auto-import 范围)。
|
||||
|
||||
Reference in new issue
Block a user