982 lines
30 KiB
Vue
982 lines
30 KiB
Vue
<!--
|
||
* 病例库-临床-文章
|
||
*
|
||
* @Author: xing
|
||
* @Date: 2025-08-04 10:17:15
|
||
* @Copyright gdxz
|
||
-->
|
||
<template>
|
||
<a-modal
|
||
:title="form.articleId ? '编辑' : '添加'"
|
||
:width="'90%'"
|
||
:open="visibleFlag"
|
||
@cancel="onClose"
|
||
:maskClosable="false"
|
||
:destroyOnClose="true"
|
||
:style="{ maxWidth: '1200px', minWidth: '800px' }"
|
||
:bodyStyle="{ maxHeight: '70vh', overflow: 'auto' }"
|
||
>
|
||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" >
|
||
<a-form-item label="标题" name="articleTitle">
|
||
<a-input style="width: 100%" v-model:value="form.articleTitle" placeholder="标题" />
|
||
</a-form-item>
|
||
<a-form-item label="状态" name="articleStatus">
|
||
<SmartEnumSelect
|
||
width="100%"
|
||
v-model:value="form.articleStatus"
|
||
enum-name="STATUS_ENUM"
|
||
placeholder=""
|
||
:disabled="!!form.articleId"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="发表时间" name="pushDate">
|
||
<a-date-picker
|
||
show-time
|
||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||
v-model:value="form.pushDate"
|
||
style="width: 100%"
|
||
placeholder="发表时间"
|
||
:disabled-date="disabledFutureDate"
|
||
:disabled-time="disabledFutureTime"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="是否外部链接" name="isLink">
|
||
<a-switch v-model:checked="isLinkChecked" @change="isLinkCheckedChange" :disabled="!!form.articleId" />
|
||
</a-form-item>
|
||
<a-form-item label="外部链接地址" name="isLinkUrl" v-if="isLinkChecked">
|
||
<a-input style="width: 100%" v-model:value="form.isLinkUrl" placeholder="外部链接地址" />
|
||
</a-form-item>
|
||
<a-form-item label="疾病标签" name="labelList">
|
||
<div class="label-container">
|
||
<!-- 标签选择器 -->
|
||
<div class="label-selector">
|
||
<a-select
|
||
v-model:value="selectedLabels[0]"
|
||
placeholder="请选择一级标签"
|
||
style="width: 200px; margin-right: 8px;"
|
||
:options="labelOptions[0]"
|
||
:field-names="{ label: 'labelName', value: 'appIden' }"
|
||
@change="onLabelChange(0)"
|
||
@dropdownVisibleChange="onLabelDropdownVisibleChange(0, $event)"
|
||
:loading="labelLoading[0]"
|
||
allowClear
|
||
/>
|
||
<a-select
|
||
v-model:value="selectedLabels[1]"
|
||
placeholder="请选择二级标签"
|
||
style="width: 200px; margin-right: 8px;"
|
||
:options="labelOptions[1]"
|
||
:field-names="{ label: 'labelName', value: 'appIden' }"
|
||
@change="onLabelChange(1)"
|
||
:loading="labelLoading[1]"
|
||
:disabled="!selectedLabels[0]"
|
||
allowClear
|
||
/>
|
||
<a-select
|
||
v-model:value="selectedLabels[2]"
|
||
placeholder="请选择三级标签"
|
||
style="width: 200px; margin-right: 8px;"
|
||
:options="labelOptions[2]"
|
||
:field-names="{ label: 'labelName', value: 'appIden' }"
|
||
@change="onLabelChange(2)"
|
||
:loading="labelLoading[2]"
|
||
:disabled="!selectedLabels[1]"
|
||
allowClear
|
||
/>
|
||
<a-button
|
||
type="primary"
|
||
@click="addSelectedLabel"
|
||
:disabled="!hasSelectedLabel"
|
||
style="margin-left: 8px;"
|
||
>
|
||
添加选中标签
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 已选择的标签列表 -->
|
||
<div class="selected-labels" v-if="form.labelList && form.labelList.length > 0">
|
||
<div class="label-item" v-for="(label, index) in form.labelList" :key="index">
|
||
<span class="label-name">{{ label.labelName }}</span>
|
||
<a-button type="link" danger size="small" @click="removeLabel(index)">
|
||
<template #icon><DeleteOutlined /></template>
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item label="二维码" name="shareQrcode">
|
||
<div class="qrcode-container">
|
||
<div class="qrcode-image" v-if="form.shareQrcode && form.shareQrcode.trim()">
|
||
<img :src="form.shareQrcode" alt="分享二维码" style="width: 120px; height: 120px; border: 1px solid #d9d9d9; border-radius: 4px;" />
|
||
</div>
|
||
<div class="qrcode-placeholder" v-else style="width: 120px; height: 120px; border: 1px dashed #d9d9d9; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px;">
|
||
暂无二维码
|
||
</div>
|
||
<a-button
|
||
v-if="!form.shareQrcode || !form.shareQrcode.trim()"
|
||
type="primary"
|
||
@click="generateQrcode"
|
||
:loading="qrcodeLoading"
|
||
style="margin-left: 12px;"
|
||
:disabled="!form.articleId"
|
||
>
|
||
生成二维码
|
||
</a-button>
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item label="作者" name="authorList">
|
||
<div class="author-container">
|
||
<!-- 作者列表 -->
|
||
<div class="author-list" v-if="form.authorList && form.authorList.length > 0">
|
||
<div class="author-item" v-for="(author, index) in form.authorList" :key="author.doctorId">
|
||
<div class="author-info">
|
||
<span class="author-name">{{ author.doctorName }}</span>
|
||
<span class="author-hospital">{{ author.hospitalName }}</span>
|
||
<span class="author-location">{{ author.hospitalProvince }} {{ author.hospitalCity }}</span>
|
||
</div>
|
||
<a-button type="link" danger size="small" @click="removeAuthor(index)">
|
||
<template #icon><DeleteOutlined /></template>
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加作者按钮 -->
|
||
<a-button type="dashed" @click="showAuthorModal" style="width: 100%; margin-top: 8px;">
|
||
<template #icon><PlusOutlined /></template>
|
||
添加作者
|
||
</a-button>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="内容" name="articleContent" v-if="!isLinkChecked">
|
||
<div class="editor-container">
|
||
<div class="editor-tip">请在此处编辑文章内容,支持富文本格式、图片上传、表格等功能</div>
|
||
<!-- <UEditor v-model="form.articleContent" :id="'article_content'" /> -->
|
||
<SmartWangeditor ref="rubricRef" :modelValue="form.articleContent" :height="500" :toolbarConfig="rubricToolbarConfig"/>
|
||
|
||
</div>
|
||
</a-form-item>
|
||
</a-form>
|
||
|
||
<template #footer>
|
||
<a-space>
|
||
<a-button @click="onClose">取消</a-button>
|
||
<a-button type="primary" @click="onSubmit">保存</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-modal>
|
||
|
||
<!-- 作者选择模态框 -->
|
||
<a-modal
|
||
title="选择作者"
|
||
:open="authorModalVisible"
|
||
@cancel="closeAuthorModal"
|
||
@ok="confirmSelectAuthor"
|
||
:maskClosable="false"
|
||
:width="700"
|
||
:zIndex="1001"
|
||
:bodyStyle="{ maxHeight: '400px', overflow: 'auto' }"
|
||
>
|
||
<div class="author-search-container">
|
||
<a-input-search
|
||
v-model:value="authorSearchKeyword"
|
||
placeholder="搜索医生姓名或医院名称"
|
||
@search="searchAuthors"
|
||
style="margin-bottom: 12px;"
|
||
size="small"
|
||
/>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="authorColumns"
|
||
:dataSource="authorList"
|
||
:loading="authorLoading"
|
||
:pagination="false"
|
||
size="small"
|
||
rowKey="doctorId"
|
||
:scroll="{ y: 250 }"
|
||
:row-selection="{
|
||
selectedRowKeys: selectedAuthorIds,
|
||
onChange: onAuthorSelectChange,
|
||
getCheckboxProps: getAuthorCheckboxProps
|
||
}"
|
||
>
|
||
<template #bodyCell="{ text, record }">
|
||
<template v-if="record.doctorName">
|
||
<div class="author-table-info">
|
||
<div class="author-name">{{ record.doctorName }}</div>
|
||
<div class="author-hospital">{{ record.hospitalName }}</div>
|
||
<div class="author-location">{{ record.hospitalProvince }} {{ record.hospitalCity }}</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</a-modal>
|
||
</template>
|
||
<script setup>
|
||
import { reactive, ref, nextTick, computed } from 'vue';
|
||
import _ from 'lodash';
|
||
import { message, Modal } from 'ant-design-vue';
|
||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||
import { caseClinicalArticleApi } from '/@/api/business/case-clinical-article/case-clinical-article-api';
|
||
import { caseClinicalDoctorApi } from '/@/api/business/case-clinical-doctor/case-clinical-doctor-api';
|
||
import { smartSentry } from '/@/lib/smart-sentry';
|
||
import FileUpload from '/@/components/support/file-upload/index.vue';
|
||
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
|
||
//import UEditor from '/@/components/business/ueditor.vue';
|
||
import SmartWangeditor from '/@/components/framework/wangeditor/index.vue';
|
||
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
|
||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||
import dayjs from 'dayjs';
|
||
|
||
// ------------------------ 事件 ------------------------
|
||
|
||
const emits = defineEmits(['reloadList']);
|
||
|
||
// ------------------------ 显示与隐藏 ------------------------
|
||
// 是否显示
|
||
const visibleFlag = ref(false);
|
||
const rubricRef = ref();
|
||
|
||
|
||
const rubricToolbarConfig = {
|
||
toolbarKeys : ['bold', 'underline', 'italic', 'through', 'code', 'sub', 'sup', 'clearStyle', 'color', 'bgColor', 'fontSize', 'fontFamily', 'indent', 'delIndent', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'lineHeight', 'insertImage', 'deleteImage', 'editImage', 'divider', 'insertLink', 'editLink', 'unLink', 'viewLink', 'codeBlock', 'blockquote', 'headerSelect', 'redo', 'undo', 'fullScreen', 'enter', 'bulletedList', 'numberedList','uploadImage' ]
|
||
}
|
||
async function show(rowData) {
|
||
// 重置表单数据
|
||
Object.assign(form, formDefault);
|
||
|
||
// 清空标签选择器数据
|
||
selectedLabels.value = ['', '', ''];
|
||
selectedLabelNames.value = ['', '', ''];
|
||
labelOptions.value = [[], [], []];
|
||
|
||
if (rowData && !_.isEmpty(rowData)) {
|
||
// 如果是编辑模式,通过getDetail接口获取完整数据
|
||
if (rowData.articleId) {
|
||
try {
|
||
SmartLoading.show();
|
||
const result = await caseClinicalArticleApi.getDetail(rowData.articleId);
|
||
if (result.data) {
|
||
Object.assign(form, result.data);
|
||
// 处理作者数据,将 caseClinicalArticleAuthor 转换为 authorList
|
||
if (result.data.caseClinicalArticleAuthor && result.data.caseClinicalArticleAuthor.length > 0) {
|
||
form.authorList = result.data.caseClinicalArticleAuthor.map(author => ({
|
||
doctorId: author.caseClinicalDoctor?.doctorId,
|
||
doctorName: author.caseClinicalDoctor?.doctorName,
|
||
hospitalName: author.caseClinicalDoctor?.hospitalName,
|
||
hospitalProvince: author.caseClinicalDoctor?.hospitalProvince,
|
||
hospitalCity: author.caseClinicalDoctor?.hospitalCity
|
||
}));
|
||
}
|
||
|
||
// 确保二维码字段正确显示
|
||
if (result.data.shareQrcode) {
|
||
form.shareQrcode = result.data.shareQrcode;
|
||
console.log('二维码地址:', result.data.shareQrcode);
|
||
}
|
||
|
||
// 处理标签数据 - 从caseClinicalArticleLabel字段读取
|
||
if (result.data.caseClinicalArticleLabel && result.data.caseClinicalArticleLabel.length > 0) {
|
||
form.labelList = result.data.caseClinicalArticleLabel.map(label => ({
|
||
appIden: label.appIden,
|
||
labelName: label.labelName
|
||
}));
|
||
console.log('加载的标签数据:', form.labelList);
|
||
} else {
|
||
// 如果没有标签数据,确保labelList为空数组
|
||
form.labelList = [];
|
||
}
|
||
}
|
||
} catch (error) {
|
||
smartSentry.captureError(error);
|
||
message.error('获取文章详情失败');
|
||
return;
|
||
} finally {
|
||
SmartLoading.hide();
|
||
}
|
||
} else {
|
||
// 如果是新增模式,直接使用传入的数据
|
||
Object.assign(form, rowData);
|
||
// 确保新增时articleId为空,labelList和authorList为空数组
|
||
form.articleId = undefined;
|
||
form.labelList = [];
|
||
form.authorList = [];
|
||
}
|
||
} else {
|
||
// 如果没有传入数据,确保articleId为空,labelList和authorList为空数组
|
||
form.articleId = undefined;
|
||
form.labelList = [];
|
||
form.authorList = [];
|
||
}
|
||
|
||
// 设置开关状态
|
||
isLinkChecked.value = form.isLink === 1;
|
||
// 使用字典时把下面这注释修改成自己的字典字段 有多个字典字段就复制多份同理修改 不然打开表单时不显示字典初始值
|
||
// if (form.status && form.status.length > 0) {
|
||
// form.status = form.status.map((e) => e.valueCode);
|
||
// }
|
||
visibleFlag.value = true;
|
||
// 不自动加载标签,等用户点击下拉框时再加载
|
||
nextTick(() => {
|
||
formRef.value.clearValidate();
|
||
});
|
||
}
|
||
|
||
function onClose() {
|
||
Object.assign(form, formDefault);
|
||
isLinkChecked.value = false;
|
||
visibleFlag.value = false;
|
||
// 清空作者相关数据
|
||
authorList.value = [];
|
||
selectedAuthorIds.value = [];
|
||
selectedAuthors.value = [];
|
||
// 清空标签相关数据
|
||
selectedLabels.value = ['', '', ''];
|
||
selectedLabelNames.value = ['', '', ''];
|
||
labelOptions.value = [[], [], []];
|
||
// 确保labelList被清空
|
||
form.labelList = [];
|
||
}
|
||
|
||
// ------------------------ 表单 ------------------------
|
||
|
||
// 组件ref
|
||
const formRef = ref();
|
||
|
||
|
||
const formDefault = {
|
||
articleTitle: undefined, //标题
|
||
articleStatus: 1, //状态(1:正常 2:禁用)- 默认为正常
|
||
|
||
pushDate: new Date().toISOString().slice(0, 19).replace('T', ' '), //发表时间,默认为当天
|
||
isLink: 0, //是否外部链接(0:否 1:是)
|
||
isLinkUrl: undefined, //外部链接地址
|
||
shareQrcode: undefined, //分享二维码地址
|
||
articleContent: '', //内容
|
||
articleContentText: '', //纯文本内容
|
||
authorList: [], //作者列表
|
||
labelList: [], //标签列表
|
||
};
|
||
|
||
let form = reactive({ ...formDefault });
|
||
|
||
// 是否外部链接开关状态
|
||
const isLinkChecked = ref(false);
|
||
|
||
// 禁用未来日期
|
||
function disabledFutureDate(current) {
|
||
// 禁用今天之后的日期
|
||
return current && current > dayjs().endOf('day');
|
||
}
|
||
|
||
// 禁用未来时间
|
||
function disabledFutureTime(date) {
|
||
if (date) {
|
||
const today = dayjs();
|
||
const selectedDate = dayjs(date);
|
||
|
||
// 如果是今天,禁用未来时间
|
||
if (selectedDate.isSame(today, 'day')) {
|
||
return {
|
||
disabledHours: () => {
|
||
const hours = [];
|
||
for (let i = today.hour() + 1; i < 24; i++) {
|
||
hours.push(i);
|
||
}
|
||
return hours;
|
||
},
|
||
disabledMinutes: (hour) => {
|
||
if (hour === today.hour()) {
|
||
const minutes = [];
|
||
for (let i = today.minute() + 1; i < 60; i++) {
|
||
minutes.push(i);
|
||
}
|
||
return minutes;
|
||
}
|
||
return [];
|
||
},
|
||
disabledSeconds: (hour, minute) => {
|
||
if (hour === today.hour() && minute === today.minute()) {
|
||
const seconds = [];
|
||
for (let i = today.second() + 1; i < 60; i++) {
|
||
seconds.push(i);
|
||
}
|
||
return seconds;
|
||
}
|
||
return [];
|
||
}
|
||
};
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
// 二维码相关数据
|
||
const qrcodeLoading = ref(false);
|
||
|
||
// 疾病标签相关数据
|
||
const labelOptions = ref([[], [], []]); // 三级标签选项
|
||
const labelLoading = ref([false, false, false]); // 三级标签加载状态
|
||
const selectedLabels = ref(['', '', '']); // 三级标签选择值
|
||
const selectedLabelNames = ref(['', '', '']); // 三级标签选择名称
|
||
|
||
// 开关状态变化处理
|
||
function isLinkCheckedChange(checked) {
|
||
form.isLink = checked ? 1 : 0;
|
||
if (checked) {
|
||
// 切换到外部链接模式时,清空内容字段
|
||
form.articleContent = '';
|
||
form.articleContentText = '';
|
||
} else {
|
||
// 切换到非外部链接模式时,清空链接地址
|
||
form.isLinkUrl = undefined;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 获取疾病标签数据
|
||
async function loadLabelOptions(level, pId = '0') {
|
||
labelLoading.value[level] = true;
|
||
try {
|
||
const result = await caseClinicalArticleApi.getCaseLabel(pId);
|
||
if (result.data) {
|
||
labelOptions.value[level] = result.data;
|
||
}
|
||
} catch (error) {
|
||
smartSentry.captureError(error);
|
||
message.error('获取标签数据失败');
|
||
} finally {
|
||
labelLoading.value[level] = false;
|
||
}
|
||
}
|
||
|
||
// 标签选择器下拉框显示事件 - 延迟加载一级标签
|
||
async function onLabelDropdownVisibleChange(level, visible) {
|
||
// 只有当下拉框显示、是第一级、没有数据且不在加载中时才请求
|
||
if (visible && level === 0 && labelOptions.value[0].length === 0 && !labelLoading.value[0]) {
|
||
await loadLabelOptions(0);
|
||
}
|
||
}
|
||
|
||
// 标签选择变化处理
|
||
async function onLabelChange(level) {
|
||
// 清空后续级别的选择
|
||
for (let i = level + 1; i < 3; i++) {
|
||
selectedLabels.value[i] = '';
|
||
selectedLabelNames.value[i] = '';
|
||
labelOptions.value[i] = [];
|
||
}
|
||
|
||
if (selectedLabels.value[level]) {
|
||
// 获取选中标签的名称
|
||
const selectedOption = labelOptions.value[level].find(option => option.appIden === selectedLabels.value[level]);
|
||
if (selectedOption) {
|
||
selectedLabelNames.value[level] = selectedOption.labelName;
|
||
|
||
// 如果选中的标签有子标签,加载下一级
|
||
if (selectedOption.isSub === 1 && level < 2) {
|
||
// 只有当下级没有数据且不在加载中时才请求
|
||
if (labelOptions.value[level + 1].length === 0 && !labelLoading.value[level + 1]) {
|
||
await loadLabelOptions(level + 1, selectedLabels.value[level]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算是否有选中的标签
|
||
const hasSelectedLabel = computed(() => {
|
||
return selectedLabels.value.some(label => label);
|
||
});
|
||
|
||
// 添加选中的标签
|
||
function addSelectedLabel() {
|
||
// 收集所有选中的标签(一级、二级、三级)
|
||
const labelsToAdd = [];
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
if (selectedLabels.value[i] && selectedLabelNames.value[i]) {
|
||
labelsToAdd.push({
|
||
appIden: selectedLabels.value[i],
|
||
labelName: selectedLabelNames.value[i]
|
||
});
|
||
}
|
||
}
|
||
|
||
if (labelsToAdd.length === 0) {
|
||
message.warning('请先选择标签');
|
||
return;
|
||
}
|
||
|
||
// 检查重复标签并添加新标签
|
||
let addedCount = 0;
|
||
let skippedCount = 0;
|
||
|
||
for (const label of labelsToAdd) {
|
||
// 检查是否已经添加过
|
||
const exists = form.labelList.some(existingLabel => existingLabel.appIden === label.appIden);
|
||
if (exists) {
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 添加到标签列表
|
||
form.labelList.push(label);
|
||
addedCount++;
|
||
}
|
||
|
||
// 显示添加结果
|
||
if (addedCount > 0) {
|
||
message.success(`成功添加 ${addedCount} 个标签`);
|
||
}
|
||
if (skippedCount > 0) {
|
||
message.warning(`跳过 ${skippedCount} 个重复标签`);
|
||
}
|
||
|
||
// 清空选择
|
||
selectedLabels.value = ['', '', ''];
|
||
selectedLabelNames.value = ['', '', ''];
|
||
labelOptions.value[1] = [];
|
||
labelOptions.value[2] = [];
|
||
}
|
||
|
||
// 移除标签
|
||
function removeLabel(index) {
|
||
form.labelList.splice(index, 1);
|
||
}
|
||
|
||
// 生成二维码
|
||
async function generateQrcode() {
|
||
if (!form.articleId) {
|
||
message.warning('请先保存文章后再生成二维码');
|
||
return;
|
||
}
|
||
|
||
// 显示确认弹窗
|
||
Modal.confirm({
|
||
title: '确认生成二维码',
|
||
content: '确定要为该文章生成分享二维码吗?',
|
||
okText: '确定',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
qrcodeLoading.value = true;
|
||
try {
|
||
await caseClinicalArticleApi.addUnlimitedQrcode(form.articleId);
|
||
message.success('二维码生成成功');
|
||
// 重新加载表单数据以获取最新的二维码地址
|
||
if (form.articleId) {
|
||
const result = await caseClinicalArticleApi.getDetail(form.articleId);
|
||
if (result.data) {
|
||
Object.assign(form, result.data);
|
||
// 重新处理作者数据
|
||
if (result.data.caseClinicalArticleAuthor && result.data.caseClinicalArticleAuthor.length > 0) {
|
||
form.authorList = result.data.caseClinicalArticleAuthor.map(author => ({
|
||
doctorId: author.caseClinicalDoctor?.doctorId,
|
||
doctorName: author.caseClinicalDoctor?.doctorName,
|
||
hospitalName: author.caseClinicalDoctor?.hospitalName,
|
||
hospitalProvince: author.caseClinicalDoctor?.hospitalProvince,
|
||
hospitalCity: author.caseClinicalDoctor?.hospitalCity
|
||
}));
|
||
}
|
||
|
||
// 重新处理标签数据
|
||
if (result.data.caseClinicalArticleLabel && result.data.caseClinicalArticleLabel.length > 0) {
|
||
form.labelList = result.data.caseClinicalArticleLabel.map(label => ({
|
||
appIden: label.appIden,
|
||
labelName: label.labelName
|
||
}));
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
smartSentry.captureError(error);
|
||
message.error('二维码生成失败');
|
||
} finally {
|
||
qrcodeLoading.value = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------ 作者相关 ------------------------
|
||
|
||
// 作者选择模态框
|
||
const authorModalVisible = ref(false);
|
||
const authorList = ref([]);
|
||
const authorLoading = ref(false);
|
||
const authorSearchKeyword = ref('');
|
||
const selectedAuthorIds = ref([]);
|
||
const selectedAuthors = ref([]);
|
||
|
||
// 作者表格列定义
|
||
const authorColumns = [
|
||
{
|
||
title: '医生信息',
|
||
dataIndex: 'doctorName',
|
||
key: 'doctorName',
|
||
width: '100%',
|
||
}
|
||
];
|
||
|
||
// 显示作者选择模态框
|
||
function showAuthorModal() {
|
||
authorModalVisible.value = true;
|
||
authorSearchKeyword.value = '';
|
||
selectedAuthorIds.value = [];
|
||
selectedAuthors.value = [];
|
||
searchAuthors();
|
||
}
|
||
|
||
// 关闭作者选择模态框
|
||
function closeAuthorModal() {
|
||
authorModalVisible.value = false;
|
||
}
|
||
|
||
// 搜索作者
|
||
async function searchAuthors() {
|
||
authorLoading.value = true;
|
||
try {
|
||
const result = await caseClinicalDoctorApi.queryList({
|
||
keywords: authorSearchKeyword.value,
|
||
limit: 8
|
||
});
|
||
authorList.value = result.data || [];
|
||
} catch (error) {
|
||
smartSentry.captureError(error);
|
||
message.error('获取医生列表失败');
|
||
} finally {
|
||
authorLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 作者选择变化
|
||
function onAuthorSelectChange(selectedRowKeys, selectedRows) {
|
||
selectedAuthorIds.value = selectedRowKeys;
|
||
selectedAuthors.value = selectedRows;
|
||
}
|
||
|
||
// 获取复选框属性(已选择的作者不可重复选择)
|
||
function getAuthorCheckboxProps(record) {
|
||
const existingAuthorIds = form.authorList.map(author => author.doctorId);
|
||
return {
|
||
disabled: existingAuthorIds.includes(record.doctorId)
|
||
};
|
||
}
|
||
|
||
// 确认选择作者
|
||
function confirmSelectAuthor() {
|
||
if (selectedAuthors.value.length === 0) {
|
||
message.warning('请选择至少一个作者');
|
||
return;
|
||
}
|
||
|
||
// 转换为表单格式并添加到作者列表
|
||
const authorForms = selectedAuthors.value.map(author => ({
|
||
doctorId: author.doctorId,
|
||
doctorName: author.doctorName,
|
||
hospitalName: author.hospitalName,
|
||
hospitalProvince: author.hospitalProvince,
|
||
hospitalCity: author.hospitalCity
|
||
}));
|
||
|
||
form.authorList.push(...authorForms);
|
||
closeAuthorModal();
|
||
}
|
||
|
||
// 移除作者
|
||
function removeAuthor(index) {
|
||
form.authorList.splice(index, 1);
|
||
}
|
||
|
||
// 表单验证规则
|
||
const rules = computed(() => ({
|
||
articleTitle: [{ required: true, message: '标题 必填' }],
|
||
articleStatus: [{ required: true, message: '状态(1:正常 2:禁用) 必填' }],
|
||
pushDate: [
|
||
{ required: true, message: '发表时间 必填' },
|
||
{
|
||
validator: (rule, value) => {
|
||
if (!value) {
|
||
return Promise.resolve();
|
||
}
|
||
const selectedDate = new Date(value);
|
||
const currentDate = new Date();
|
||
// 设置当前时间为当天的23:59:59,允许选择当天
|
||
currentDate.setHours(23, 59, 59, 999);
|
||
|
||
if (selectedDate > currentDate) {
|
||
return Promise.reject('发表时间不能大于当前时间');
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
}
|
||
],
|
||
authorList: [
|
||
{
|
||
required: true,
|
||
message: '作者 必填',
|
||
validator: (rule, value) => {
|
||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||
return Promise.reject('请至少选择一个作者');
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
}
|
||
],
|
||
isLinkUrl: [{
|
||
required: isLinkChecked.value,
|
||
message: '外部链接地址 必填',
|
||
validator: (rule, value) => {
|
||
if (!isLinkChecked.value) {
|
||
return Promise.resolve();
|
||
}
|
||
if (!value || value.trim() === '') {
|
||
return Promise.reject('外部链接地址 必填');
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
}],
|
||
articleContent: [{
|
||
required: !isLinkChecked.value,
|
||
message: '内容 必填',
|
||
validator: (rule, value) => {
|
||
if (isLinkChecked.value) {
|
||
return Promise.resolve();
|
||
}
|
||
if (!value || value.trim() === '') {
|
||
return Promise.reject('内容 必填');
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
}],
|
||
}));
|
||
|
||
// 点击确定,验证表单
|
||
async function onSubmit() {
|
||
// 只有在非外部链接模式下才获取编辑器内容
|
||
if (!isLinkChecked.value && rubricRef.value) {
|
||
form.articleContent = rubricRef.value.getHtml();
|
||
}
|
||
|
||
try {
|
||
// 只有在非外部链接模式下才获取内容
|
||
if (!isLinkChecked.value) {
|
||
// UEditor通过v-model自动同步,不需要手动获取
|
||
// 如果需要纯文本,可以从HTML中提取
|
||
if (form.articleContent) {
|
||
// 简单的HTML标签移除,提取纯文本
|
||
form.articleContentText = form.articleContent.replace(/<[^>]*>/g, '');
|
||
}
|
||
}
|
||
|
||
await formRef.value.validateFields();
|
||
save();
|
||
} catch (err) {
|
||
console.error(err);
|
||
message.error('参数验证错误,请仔细填写表单数据!');
|
||
}
|
||
}
|
||
|
||
// 新建、编辑API
|
||
async function save() {
|
||
SmartLoading.show();
|
||
try {
|
||
if (form.articleId) {
|
||
await caseClinicalArticleApi.update(form);
|
||
} else {
|
||
await caseClinicalArticleApi.add(form);
|
||
}
|
||
message.success('操作成功');
|
||
emits('reloadList');
|
||
onClose();
|
||
} catch (err) {
|
||
smartSentry.captureError(err);
|
||
} finally {
|
||
SmartLoading.hide();
|
||
}
|
||
}
|
||
|
||
defineExpose({
|
||
show,
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.author-container {
|
||
width: 100%;
|
||
}
|
||
|
||
.author-list {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.author-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 6px;
|
||
margin-bottom: 8px;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.author-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.author-name {
|
||
font-weight: 500;
|
||
color: #262626;
|
||
}
|
||
|
||
.author-hospital {
|
||
color: #595959;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.author-location {
|
||
color: #8c8c8c;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.author-table-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
}
|
||
|
||
.author-table-info .author-name {
|
||
font-weight: 500;
|
||
color: #262626;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.author-table-info .author-hospital {
|
||
color: #595959;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.author-table-info .author-location {
|
||
color: #8c8c8c;
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 弹窗表格样式优化 */
|
||
:deep(.ant-table-tbody > tr > td) {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
:deep(.ant-table-thead > tr > th) {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 响应式样式 */
|
||
@media (max-width: 768px) {
|
||
:deep(.ant-modal) {
|
||
margin: 16px;
|
||
max-width: calc(100vw - 32px) !important;
|
||
}
|
||
|
||
:deep(.ant-form-item-label) {
|
||
text-align: left !important;
|
||
}
|
||
|
||
:deep(.ant-form-item-label > label) {
|
||
height: auto !important;
|
||
line-height: 1.5 !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
:deep(.ant-modal-body) {
|
||
padding: 12px !important;
|
||
}
|
||
|
||
.author-item {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.author-info {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.qrcode-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.qrcode-image img {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.qrcode-image img:hover {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.label-container {
|
||
width: 100%;
|
||
}
|
||
|
||
.label-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.selected-labels {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.label-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 4px 8px;
|
||
background-color: #f0f0f0;
|
||
border-radius: 4px;
|
||
border: 1px solid #d9d9d9;
|
||
}
|
||
|
||
.label-name {
|
||
margin-right: 4px;
|
||
font-size: 12px;
|
||
color: #333;
|
||
}
|
||
|
||
.editor-container {
|
||
position: relative;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
background-color: #fafafa;
|
||
min-height: 200px; /* 确保编辑器有最小高度 */
|
||
}
|
||
|
||
.editor-tip {
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 10px;
|
||
background-color: #fff;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
z-index: 1;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
</style>
|