Files
xk-doctor-wx/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
2026-08-06 09:41:06 +08:00

1008 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!--
AI辅助出方单根 u-popupVue2 组件不能双根
生成确认用抽屉内全屏覆盖层避免第二个 popup / page-container 点了无反应
模板禁止 ?? ?. :class 禁止方法调用
-->
<u-popup
v-model="visible"
mode="bottom"
border-radius="24"
:safe-area-inset-bottom="true"
:mask-close-able="true"
height="82%"
z-index="10090"
@close="onClose"
>
<view class="ai-rx-drawer">
<view class="ai-rx-title-row">
<text class="ai-rx-title">AI辅助出方</text>
<text class="ai-rx-sub">{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || 0 }}</text>
</view>
<view v-if="conflictView.length && !genModalVisible" class="ai-conflict">
<text class="ai-conflict-title">配伍禁忌提示</text>
<view v-for="(line, ci) in conflictView" :key="ci" class="ai-conflict-line">· {{ line }}</view>
</view>
<!-- 主内容历史 + 对照 -->
<view v-if="!genModalVisible" class="ai-rx-body flex-row">
<scroll-view scroll-y class="ai-rx-hist">
<view class="ai-hist-hd">
<text class="fs-24 font-bold">历史</text>
</view>
<view v-if="histLoading" class="p-24 color-sub fs-24">加载中</view>
<view v-else-if="historyEmpty" class="p-24 color-sub fs-22">暂无记录可生成处方</view>
<view
v-for="row in historyView"
:key="row.id"
class="ai-hist-item"
:class="{ 'ai-hist-item--active': activeId == row.id }"
:data-id="row.id"
@click="onSelectHistoryTap"
>
<text class="fs-26 font-bold ai-hist-name">{{ row.nameText }}</text>
<text class="fs-22 color-sub">{{ row.typeText }}</text>
</view>
</scroll-view>
<scroll-view scroll-y class="ai-rx-match">
<view v-if="generating || matchLoading" class="p-32 color-sub">AI 处理中</view>
<block v-else>
<view v-if="!matchResult" class="p-32 color-sub fs-24">请选择历史或生成处方</view>
<block v-else>
<view class="ai-sec-title">已对照{{ matchedCount }}</view>
<view v-if="isTcmRx" class="ai-tcm-grid">
<view v-for="m in matchedView" :key="m.mi" class="ai-tcm-cell">
<view class="flex-row flex-ali-center flex-jus-sp">
<text class="fs-24 font-bold ai-match-name">{{ m.ai_name }}</text>
<switch
:checked="m.importOn"
color="#6ACDBB"
style="transform: scale(0.6)"
:data-mi="m.mi"
@change="onToggleImportTap"
/>
</view>
<text class="fs-20 color-sub">{{ m.doseUnit }}{{ m.usageText }}</text>
<view class="flex-row flex-wrap m-t-8">
<view
v-for="c in m.candidates"
:key="c.drug_id"
class="ai-cand ai-cand--sm"
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
:data-mi="m.mi"
:data-drug-id="c.drug_id"
@click="onPickCandidateTap"
>
<text class="fs-18">{{ c.drug_name }}</text>
</view>
</view>
</view>
</view>
<block v-else>
<view v-for="m in matchedView" :key="m.mi" class="ai-match-row ai-match-row--card">
<view class="flex-row flex-ali-center flex-jus-sp">
<text class="fs-26 font-bold ai-match-name">{{ m.ai_name }}</text>
<text class="fs-22 color-sub">{{ m.doseUnit }}{{ m.usageText }}</text>
<switch
:checked="m.importOn"
color="#6ACDBB"
style="transform: scale(0.65)"
:data-mi="m.mi"
@change="onToggleImportTap"
/>
</view>
<view class="flex-row flex-wrap m-t-8">
<view
v-for="c in m.candidates"
:key="c.drug_id"
class="ai-cand ai-cand--sm"
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
:data-mi="m.mi"
:data-drug-id="c.drug_id"
@click="onPickCandidateTap"
>
<text class="fs-20">{{ c.drug_name }}</text>
</view>
</view>
</view>
</block>
<view class="ai-sec-title m-t-16">未对照{{ unmatchedCount }}</view>
<view
v-for="u in unmatchedView"
:key="u.ui"
class="ai-match-row ai-match-row--warn"
>
<text class="fs-26">{{ u.ai_name }}</text>
<text class="ai-link m-t-8" :data-ui="u.ui" @click="onSearchUnmatchedTap">去手动搜索</text>
</view>
<view
v-if="rxPrescriptionName || rxReason || rxBasis || rxDosage || rxDayDosage"
class="ai-rx-meta m-t-16"
>
<view class="ai-desc-row ai-desc-row--full">
<text class="ai-desc-label">药方名</text>
<text class="ai-desc-val">{{ rxPrescriptionName || '未命名药方' }}</text>
</view>
<view class="ai-desc-row">
<text class="ai-desc-label">推荐剂数</text>
<text class="ai-desc-val">{{ rxDosage || '—' }}</text>
</view>
<view class="ai-desc-row">
<text class="ai-desc-label">每天几次</text>
<text class="ai-desc-val">{{ rxDayDosage || '—' }}</text>
</view>
<view v-if="rxReason" class="ai-desc-row ai-desc-row--full">
<text class="ai-desc-label">为什么出这个方</text>
<text class="ai-desc-val">{{ rxReason }}</text>
</view>
<view v-if="rxBasis" class="ai-desc-row ai-desc-row--full">
<text class="ai-desc-label">根据哪些</text>
<text class="ai-desc-val">{{ rxBasis }}</text>
</view>
</view>
</block>
</block>
</scroll-view>
</view>
<!-- 二级抽屉内全屏生成面板不用第二个 popup -->
<view v-else class="ai-gen-panel">
<view class="ai-gen-hd flex-row flex-jus-sp flex-ali-center">
<text class="fs-30 font-bold">生成处方</text>
<text class="ai-rx-sub" @click="closeGenModal">返回</text>
</view>
<text class="ai-gen-tip">点击文字可编辑将同步到病历 · {{ typeLabel }}</text>
<scroll-view scroll-y class="ai-gen-scroll">
<view class="ai-gen-grid">
<view
v-for="row in genPreviewView"
:key="row.key"
class="ai-gen-row"
:class="{ 'ai-gen-row--full': row.key == 'chief_complaint' }"
>
<text class="ai-gen-label">{{ row.label }}</text>
<textarea
v-if="editingKey == row.key"
class="ai-gen-textarea"
:value="row.value"
:data-key="row.key"
auto-height
maxlength="2000"
@input="onGenFieldInput"
@blur="onGenFieldBlur"
/>
<text
v-else
class="ai-gen-val"
:data-key="row.key"
@click="onStartEditTap"
>{{ row.value || '(空,点击填写)' }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="ai-rx-footer safe-area-inset-bottom">
<view v-if="!genModalVisible" class="ai-actions">
<view class="ai-btn ai-btn--ghost" @click="onClose">关闭</view>
<view class="ai-btn ai-btn--ghost" @click="openGenModal">生成处方</view>
<view class="ai-btn ai-btn--primary" @click="onConfirmImport">确认导入</view>
</view>
<view v-else class="ai-actions">
<view class="ai-btn ai-btn--ghost" @click="closeGenModal">返回</view>
<view class="ai-btn ai-btn--primary" @click="onGenerate">
{{ generating ? '生成中' : '开始生成' }}
</view>
</view>
</view>
</view>
</u-popup>
</template>
<script>
import {
aiGeneratePrescriptionApi,
aiListGenerationsApi,
aiMatchPrescriptionDrugsApi,
} from '@/api/reception.js'
const MR_EDIT_FIELDS = [
{ key: 'present_illness', label: '现病史' },
{ key: 'tongue', label: '舌象' },
{ key: 'pulse', label: '脉象' },
{ key: 'tcm_case', label: '中医病案' },
{ key: 'tcm_disease', label: '中医疾病' },
{ key: 'tcm_syndrome', label: '中医证候' },
{ key: 'tcm_method', label: '中医治法' },
{ key: 'diagnosis', label: '临床诊断' },
{ key: 'treatment_advice', label: '治疗意见' },
{ key: 'doctor_order', label: '医嘱' },
{ key: 'allergy_history', label: '过敏史' },
{ key: 'past_history', label: '既往史' },
{ key: 'family_history', label: '家族史' },
{ key: 'epidemic_history', label: '流行病学史' },
{ key: 'personal_history', label: '个人史' },
{ key: 'menstrual_history', label: '月经史' },
{ key: 'marital_history', label: '婚育史' },
{ key: 'physical_exam', label: '体征检查' },
{ key: 'auxiliary_exam', label: '辅助检查' },
]
export default {
name: 'AiPrescriptionDrawer',
props: {
value: { type: Boolean, default: false },
registerId: { type: [Number, String], default: 0 },
storeId: { type: [Number, String], default: 0 },
prescriptionType: { type: [Number, String], default: 0 },
patientName: { type: String, default: '' },
patientSex: { type: [Number, String], default: 0 },
patientAge: { type: [Number, String], default: 0 },
chiefComplaint: { type: String, default: '' },
medicalRecord: { type: Object, default: () => ({}) },
},
data() {
return {
visible: false,
genModalVisible: false,
aiChief: '',
localMr: {},
editingKey: '',
histLoading: false,
generating: false,
matchLoading: false,
historyList: [],
historyView: [],
historyEmpty: true,
activeId: 0,
matchResult: null,
matchedList: [],
unmatchedList: [],
matchedView: [],
unmatchedView: [],
matchedCount: 0,
unmatchedCount: 0,
conflictView: [],
rxDosage: 0,
rxDayDosage: 0,
rxReason: '',
rxBasis: '',
rxPrescriptionName: '',
genPreviewView: [],
importBtnStyle: {
backgroundColor: '#6ACDBB',
color: '#fff',
border: 'none',
width: '100%',
},
}
},
computed: {
sexLabel() {
const s = Number(this.patientSex)
if (s === 1) return '男'
if (s === 2) return '女'
return '未知'
},
typeLabel() {
const t = Number(this.prescriptionType)
if (t === 1) return '中药'
if (t === 2) return '西(中成)药'
return t ? '类型' + t : '—'
},
isTcmRx() {
return Number(this.prescriptionType) === 1
},
},
watch: {
value: {
immediate: true,
handler(v) {
this.visible = !!v
if (v) this.bootstrapOpen()
},
},
visible(v) {
this.$emit('input', v)
this.$emit('overlay-change', !!v)
},
},
methods: {
bootstrapOpen() {
this.aiChief = String(this.chiefComplaint || '').trim()
this.localMr = Object.assign({}, this.medicalRecord || {})
this.genModalVisible = false
this.editingKey = ''
this.resetPreview()
this.loadHistory().then(() => {
if (this.historyList.length) {
this.selectHistoryById(Number(this.historyList[0].id))
} else {
// 无历史:自动打开生成框
this.openGenModal()
}
})
},
resetPreview() {
this.matchResult = null
this.matchedList = []
this.unmatchedList = []
this.conflictView = []
this.rxDosage = 0
this.rxDayDosage = 0
this.rxReason = ''
this.rxBasis = ''
this.rxPrescriptionName = ''
this.activeId = 0
this.rebuildMatchView()
},
onClose() {
this.visible = false
this.genModalVisible = false
},
/**
* Laravel jok 业务在 result兼容 Yii data / 已解包数组
*/
unwrapBiz(res) {
if (res == null) return null
if (res.result != null) return res.result
if (res.data != null) return res.data
return res
},
formatTime(ts) {
// 已是格式化字符串则直接展示(查询器可能一级格式化)
if (typeof ts === 'string' && ts && Number.isNaN(Number(ts))) {
return ts.length > 16 ? ts.slice(5, 16) : ts
}
const n = Number(ts || 0)
if (!n) return '—'
const d = new Date(n * 1000)
const pad = (x) => (x < 10 ? '0' + x : '' + x)
return pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
},
rebuildHistoryView() {
const list = Array.isArray(this.historyList) ? this.historyList : []
this.historyEmpty = list.length === 0
this.historyView = list.map((row) => {
const id = Number(row.id || 0)
const cnt = Number(row.item_count || 0)
const pt = Number(row.prescription_type || 0)
const name = String(row.name || row.prescription_name || '').trim()
let typeLabel = String(row.prescription_type_label || '').trim()
if (!typeLabel) {
if (pt === 1) typeLabel = '中药'
else if (pt === 2) typeLabel = '西药(中成药)'
else typeLabel = pt ? '类型' + pt : '—'
}
return {
id: id,
nameText: name || ('方案 #' + id),
typeText: typeLabel + ' · ' + cnt + '味 · ' + this.formatTime(row.created_at),
prescription_type: pt,
}
})
},
rebuildMatchView() {
const matched = Array.isArray(this.matchedList) ? this.matchedList : []
this.matchedCount = matched.length
this.matchedView = matched.map((m, mi) => {
const cands = Array.isArray(m.candidates) ? m.candidates : []
return {
mi: mi,
ai_name: m.ai_name || '',
doseUnit: String(m.dose || '') + String(m.unit || ''),
usageText: m.usage ? ('·' + m.usage) : '',
selected_drug_id: Number(m.selected_drug_id || 0),
importOn: m._import !== false,
candidates: cands.map((c) => ({
drug_id: Number(c.drug_id || 0),
drug_name: c.drug_name || '',
})),
}
})
const unmatched = Array.isArray(this.unmatchedList) ? this.unmatchedList : []
this.unmatchedCount = unmatched.length
this.unmatchedView = unmatched.map((u, ui) => ({
ui: ui,
ai_name: u.ai_name || '',
tip: String(u.dose || '') + String(u.unit || '') + ' · 未匹配',
}))
},
rebuildGenPreview() {
const mr = this.localMr || {}
const rows = [
{
key: 'chief_complaint',
label: '主诉',
value: String(this.aiChief || mr.chief_complaint || '').trim(),
},
]
for (let i = 0; i < MR_EDIT_FIELDS.length; i++) {
const f = MR_EDIT_FIELDS[i]
const val = String(mr[f.key] || '').trim()
if (val) rows.push({ key: f.key, label: f.label, value: val })
}
this.genPreviewView = rows
},
applyRxMeta(data) {
this.rxDosage = Number((data && data.dosage) || 0)
this.rxDayDosage = Number((data && data.day_dosage) || 0)
this.rxReason = String((data && data.reason) || '').trim()
this.rxBasis = String((data && data.basis) || '').trim()
this.rxPrescriptionName = String((data && data.prescription_name) || '').trim()
},
applyConflict(conflict) {
const msgs = (conflict && conflict.messages) || []
this.conflictView = (Array.isArray(msgs) ? msgs : [])
.map((m) => String(m || '').trim())
.filter((m) => !!m)
},
applyMatch(match) {
this.matchResult = match || null
const matched = (match && match.matched) || []
this.matchedList = (Array.isArray(matched) ? matched : []).map((m) =>
Object.assign({}, m, { _import: true }),
)
this.unmatchedList = Array.isArray(match && match.unmatched) ? match.unmatched : []
this.rebuildMatchView()
},
async loadHistory() {
this.histLoading = true
try {
const res = await aiListGenerationsApi({
register_id: Number(this.registerId),
scene: 'prescription',
limit: 30,
})
const list = this.unwrapBiz(res)
this.historyList = Array.isArray(list) ? list : []
this.rebuildHistoryView()
} catch (e) {
this.historyList = []
this.rebuildHistoryView()
const msg = (e && e.message) || (e && e.msg) || ''
if (msg) uni.showToast({ title: String(msg), icon: 'none' })
} finally {
this.histLoading = false
}
},
openGenModal() {
if (!Number(this.prescriptionType || 0)) {
uni.showToast({ title: '请先选择处方类型', icon: 'none' })
return
}
// 抽屉内切到生成面板(单根 u-popup避免 Vue2 双根第二个挂不上)
this.editingKey = ''
this.rebuildGenPreview()
this.$nextTick(() => {
this.genModalVisible = true
})
},
closeGenModal() {
this.genModalVisible = false
this.editingKey = ''
},
onStartEditTap(e) {
const key = e && e.currentTarget && e.currentTarget.dataset
? e.currentTarget.dataset.key
: ''
if (!key) return
this.editingKey = key
},
onGenFieldInput(e) {
const key = e && e.currentTarget && e.currentTarget.dataset
? e.currentTarget.dataset.key
: this.editingKey
const val = (e && e.detail && e.detail.value) || ''
if (!key) return
if (key === 'chief_complaint') {
this.aiChief = val
this.localMr = Object.assign({}, this.localMr, { chief_complaint: val })
} else {
const next = Object.assign({}, this.localMr)
next[key] = val
this.localMr = next
}
// 同步到父页病历
const payload = {}
payload[key] = val
this.$emit('sync-medical-record', payload)
// 更新预览行展示值
for (let i = 0; i < this.genPreviewView.length; i++) {
if (this.genPreviewView[i].key === key) {
this.$set(this.genPreviewView[i], 'value', val)
break
}
}
},
onGenFieldBlur() {
this.editingKey = ''
},
async onGenerate() {
if (this.generating) return
const chief = String(this.aiChief || '').trim()
if (!chief) {
uni.showToast({ title: '请填写主诉后再生成', icon: 'none' })
return
}
const type = Number(this.prescriptionType || 0)
if (!type) {
uni.showToast({ title: '请先选择处方类型', icon: 'none' })
return
}
// 先关二级弹窗,回主抽屉等待生成
this.closeGenModal()
this.generating = true
try {
const res = await aiGeneratePrescriptionApi({
register_id: Number(this.registerId),
store_id: Number(this.storeId || 0) || undefined,
prescription_type: type,
chief_complaint: chief,
medical_record: Object.assign({}, this.localMr || {}, {
chief_complaint: chief,
}),
})
const data = this.unwrapBiz(res) || {}
await this.loadHistory()
this.activeId = Number(data.generation_id || 0)
this.applyMatch(data.match || null)
this.applyConflict(data.conflict || null)
this.applyRxMeta(data)
uni.showToast({ title: '已生成', icon: 'success' })
} catch (e) {
const msg = (e && e.message) || (e && e.msg) || '生成失败'
uni.showToast({ title: String(msg), icon: 'none' })
} finally {
this.generating = false
}
},
onSelectHistoryTap(e) {
const raw = e && e.currentTarget && e.currentTarget.dataset
? e.currentTarget.dataset.id
: 0
const id = Number(raw || 0)
if (!id) return
this.selectHistoryById(id)
},
async selectHistoryById(id) {
this.activeId = id
this.matchLoading = true
try {
let pt = Number(this.prescriptionType || 0)
let histName = ''
for (let i = 0; i < this.historyList.length; i++) {
if (Number(this.historyList[i].id) === id) {
pt = Number(this.historyList[i].prescription_type || pt)
histName = String(
this.historyList[i].name || this.historyList[i].prescription_name || '',
).trim()
break
}
}
const res = await aiMatchPrescriptionDrugsApi({
generation_id: id,
register_id: Number(this.registerId),
store_id: Number(this.storeId || 0) || undefined,
prescription_type: pt,
})
const data = this.unwrapBiz(res) || {}
this.applyMatch(data)
this.applyConflict(data.conflict || null)
this.applyRxMeta({
dosage: data.dosage,
day_dosage: data.day_dosage,
reason: data.reason,
basis: data.basis,
prescription_name: data.prescription_name || histName,
})
} catch (e) {
this.matchResult = null
this.matchedList = []
this.unmatchedList = []
this.conflictView = []
this.rebuildMatchView()
const msg = (e && e.message) || (e && e.msg) || '对照失败'
uni.showToast({ title: String(msg), icon: 'none' })
} finally {
this.matchLoading = false
}
},
onPickCandidateTap(e) {
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
const mi = Number(ds.mi || 0)
const drugId = Number(ds.drugId || ds['drug-id'] || 0)
if (!this.matchedList[mi]) return
this.$set(this.matchedList[mi], 'selected_drug_id', drugId)
this.rebuildMatchView()
},
onToggleImportTap(e) {
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
const mi = Number(ds.mi || 0)
const checked = !!(e && e.detail && e.detail.value)
if (!this.matchedList[mi]) return
this.$set(this.matchedList[mi], '_import', checked)
this.rebuildMatchView()
},
onSearchUnmatchedTap(e) {
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
const ui = Number(ds.ui || 0)
const row = this.unmatchedList[ui]
this.$emit('search-drug', (row && row.ai_name) || '')
this.visible = false
},
onConfirmImport() {
const drugs = []
for (let i = 0; i < this.matchedList.length; i++) {
const m = this.matchedList[i]
if (!m || m._import === false) continue
const sid = Number(m.selected_drug_id || 0)
if (!sid) continue
const cands = m.candidates || []
let picked = null
for (let j = 0; j < cands.length; j++) {
if (Number(cands[j].drug_id) === sid) {
picked = cands[j]
break
}
}
if (!picked) continue
drugs.push({
candidate: picked,
dose: m.dose || '',
unit: m.unit || '',
usage: m.usage || '',
ai_name: m.ai_name || '',
})
}
if (!drugs.length) {
uni.showToast({ title: '请先勾选已对照药品', icon: 'none' })
return
}
this.$emit('import', {
rows: drugs,
dosage: this.rxDosage || undefined,
day_dosage: this.rxDayDosage || undefined,
})
this.visible = false
},
},
}
</script>
<style scoped lang="scss">
.ai-rx-drawer {
height: 100%;
display: flex;
flex-direction: column;
background: #f7f8fa;
position: relative;
overflow: hidden;
}
.ai-gen-panel {
flex: 1;
height: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
}
.ai-rx-title-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
background: #fff;
}
.ai-rx-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.ai-rx-sub {
font-size: 24rpx;
color: #6acdbb;
}
.ai-rx-meta {
margin: 0 0 12rpx;
background: #fff;
border-radius: 12rpx;
border: 1px solid #eee;
overflow: hidden;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.ai-desc-row {
width: 50%;
display: flex;
flex-direction: column;
padding: 12rpx 16rpx;
box-sizing: border-box;
border-bottom: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
}
.ai-desc-row--full {
width: 100%;
border-right: none;
}
.ai-desc-label {
font-size: 20rpx;
color: #999;
margin-bottom: 4rpx;
}
.ai-desc-val {
font-size: 24rpx;
color: #2a2e35;
line-height: 1.45;
white-space: pre-wrap;
}
.ai-conflict {
margin: 0 24rpx 12rpx;
padding: 16rpx;
background: #fff7e6;
border: 1px solid #ffe0b2;
border-radius: 8rpx;
}
.ai-conflict-title {
display: block;
font-size: 26rpx;
font-weight: bold;
color: #ad6800;
margin-bottom: 8rpx;
}
.ai-conflict-line {
font-size: 22rpx;
color: #874d00;
line-height: 1.5;
}
.ai-rx-body {
flex: 1;
/* 微信 scroll-view 在 flex 下需 height:0 才能吃满剩余高度 */
height: 0;
min-height: 0;
overflow: hidden;
}
.ai-rx-hist {
width: 220rpx;
background: #fff;
border-right: 1px solid #eee;
height: 100%;
}
.ai-rx-match {
flex: 1;
height: 100%;
padding: 16rpx 20rpx 24rpx;
box-sizing: border-box;
}
.ai-rx-dose-row {
margin-top: 10rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.ai-rx-dose-item {
margin-right: 32rpx;
margin-bottom: 4rpx;
font-size: 24rpx;
color: #666;
}
.ai-rx-dose-val {
font-weight: bold;
color: #2a2e35;
}
.ai-hist-hd {
padding: 16rpx;
border-bottom: 1px solid #f0f0f0;
}
.ai-hist-item {
padding: 16rpx;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.ai-hist-item--active {
background: #e8f8f4;
}
.ai-hist-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-sec-title {
font-size: 24rpx;
font-weight: bold;
color: #666;
margin-bottom: 12rpx;
}
.ai-tcm-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -6rpx;
}
.ai-tcm-cell {
width: calc(50% - 12rpx);
margin: 0 6rpx 12rpx;
padding: 12rpx;
background: #fff;
border-radius: 10rpx;
box-sizing: border-box;
}
.ai-match-row {
padding: 12rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.ai-match-row--card {
margin-bottom: 12rpx;
padding: 16rpx;
background: #fff;
border-radius: 12rpx;
border-bottom: none;
}
.ai-match-row--warn {
margin-bottom: 12rpx;
padding: 16rpx;
background: #fff7e6;
border-radius: 12rpx;
border-bottom: none;
}
.ai-match-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8rpx;
}
.ai-cand {
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 8rpx 12rpx;
margin-right: 12rpx;
margin-bottom: 8rpx;
}
.ai-cand--sm {
padding: 4rpx 10rpx;
margin-right: 8rpx;
margin-bottom: 8rpx;
}
.ai-cand--on {
border-color: #6acdbb;
background: #e8f8f4;
}
.ai-link {
color: #6acdbb;
font-size: 24rpx;
}
.ai-rx-footer {
padding: 16rpx 24rpx 24rpx;
background: #fff;
}
.ai-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 16rpx;
}
.ai-btn {
padding: 16rpx 28rpx;
border-radius: 40rpx;
font-size: 26rpx;
}
.ai-btn--ghost {
background: #f5f5f5;
color: #666;
}
.ai-btn--primary {
background: #6acdbb;
color: #fff;
}
.ai-btn--block {
display: block;
text-align: center;
}
.ai-gen-hd {
padding: 16rpx 32rpx 8rpx;
}
.ai-gen-tip {
padding: 0 32rpx 12rpx;
font-size: 22rpx;
color: #999;
}
.ai-gen-scroll {
flex: 1;
height: 0;
min-height: 0;
padding: 0 24rpx 16rpx;
box-sizing: border-box;
background: #f7f8fa;
}
.ai-gen-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -6rpx;
}
.ai-gen-row {
width: calc(50% - 12rpx);
margin: 0 6rpx 8rpx;
padding: 12rpx 16rpx;
background: #f7f8fa;
border-radius: 10rpx;
box-sizing: border-box;
}
.ai-gen-row--full {
width: calc(100% - 12rpx);
}
.ai-gen-label {
display: block;
font-size: 20rpx;
color: #999;
margin-bottom: 4rpx;
}
.ai-gen-val {
display: block;
font-size: 24rpx;
color: #2a2e35;
line-height: 1.5;
white-space: pre-wrap;
}
.ai-gen-textarea {
width: 100%;
min-height: 80rpx;
font-size: 24rpx;
color: #2a2e35;
background: #fff;
padding: 8rpx;
box-sizing: border-box;
border-radius: 6rpx;
}
.color-sub {
color: #999;
}
.p-24 {
padding: 24rpx;
}
.p-32 {
padding: 32rpx;
}
.m-t-8 {
margin-top: 8rpx;
}
.m-t-16 {
margin-top: 16rpx;
}
.fs-18 {
font-size: 18rpx;
}
.fs-20 {
font-size: 20rpx;
}
.fs-22 {
font-size: 22rpx;
}
.fs-24 {
font-size: 24rpx;
}
.fs-26 {
font-size: 26rpx;
}
.fs-30 {
font-size: 30rpx;
}
.font-bold {
font-weight: bold;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-ali-center {
align-items: center;
}
.flex-jus-sp {
justify-content: space-between;
}
</style>