Files
xk-doctor-wx/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue

998 lines
29 KiB
Vue
Raw Normal View History

<template>
<!--
AI辅助出方主弹层=历史+对照二级弹层=可编辑病历预览+生成
模板禁止 ?? ?. :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" 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 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>
<!-- 药方摘要仿 Descriptions 双列紧凑 -->
<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>
<view class="ai-rx-footer safe-area-inset-bottom">
<view 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>
</view>
</u-popup>
<!-- 二级可编辑病历预览 + 生成 page-container 防误退 -->
<page-container
v-if="genModalVisible"
:show="genModalVisible"
overlay
round
position="bottom"
@afterleave="onGenModalLeave"
@clickoverlay="closeGenModal"
>
<view class="ai-gen-sheet">
<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 class="ai-rx-footer">
<view class="ai-btn ai-btn--primary ai-btn--block" @click="onGenerate">
{{ generating ? '生成中…' : '开始生成' }}
</view>
</view>
</view>
</page-container>
</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
},
formatTime(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 = (res && res.data) || res || []
this.historyList = Array.isArray(list) ? list : []
this.rebuildHistoryView()
} catch (e) {
this.historyList = []
this.rebuildHistoryView()
} finally {
this.histLoading = false
}
},
openGenModal() {
if (!Number(this.prescriptionType || 0)) {
uni.showToast({ title: '请先选择处方类型', icon: 'none' })
return
}
this.editingKey = ''
this.rebuildGenPreview()
this.genModalVisible = true
},
closeGenModal() {
this.genModalVisible = false
this.editingKey = ''
},
onGenModalLeave() {
this.genModalVisible = false
},
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 = (res && res.data) || 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 = (res && res.data) || 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;
}
.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-sheet {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 78vh;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
}
.ai-gen-hd {
padding: 24rpx 32rpx 8rpx;
}
.ai-gen-tip {
padding: 0 32rpx 12rpx;
font-size: 22rpx;
color: #999;
}
.ai-gen-scroll {
max-height: 52vh;
padding: 0 24rpx;
box-sizing: border-box;
}
.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>