feat: 接入患者建档、档案编辑接口
This commit is contained in:
parent
17c2e25bc3
commit
7cb83882b8
@ -19,28 +19,30 @@
|
||||
<image class="pen" src="/static/icons/icon-pen.svg" />
|
||||
</view>
|
||||
<view class="rows">
|
||||
<view class="row">
|
||||
<view class="label">姓名</view>
|
||||
<view v-if="!editing" class="val">{{ data.name || '-' }}</view>
|
||||
<input v-else v-model="draft.name" class="input" placeholder="请输入姓名" />
|
||||
<form-template
|
||||
v-if="editing && effectiveBaseItems.length"
|
||||
ref="baseFormRef"
|
||||
:items="effectiveBaseItems"
|
||||
:form="forms"
|
||||
@change="onChange"
|
||||
/>
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="item in effectiveBaseItems"
|
||||
:key="item.title"
|
||||
class="row"
|
||||
>
|
||||
<view class="label">{{ item.name || item.title }}</view>
|
||||
<view
|
||||
v-if="item.title === 'mobile' && displayValue(item) !== '-'"
|
||||
class="val link"
|
||||
@click="call(rawValue(item))"
|
||||
>
|
||||
{{ displayValue(item) }}
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">性别</view>
|
||||
<view v-if="!editing" class="val">{{ data.sex || '-' }}</view>
|
||||
<picker v-else mode="selector" :range="sexOptions" @change="pickSex">
|
||||
<view class="picker">{{ draft.sex || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">年龄</view>
|
||||
<view v-if="!editing" class="val">{{ data.age || '-' }}</view>
|
||||
<input v-else v-model="draft.age" class="input" type="number" placeholder="请输入年龄" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">联系电话</view>
|
||||
<view v-if="!editing" class="val link" @click="call(data.mobile)">{{ data.mobile || '-' }}</view>
|
||||
<input v-else v-model="draft.mobile" class="input" type="number" placeholder="请输入联系电话" />
|
||||
<view v-else class="val" :class="displayValue(item) === '-' ? 'muted' : ''">{{ displayValue(item) }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -53,35 +55,28 @@
|
||||
<view class="row" @click="openTransferRecord">
|
||||
<view class="label">院内来源</view>
|
||||
<view class="val link">
|
||||
{{ data.creator || '点击查看' }}
|
||||
{{ forms.creator || '点击查看' }}
|
||||
<uni-icons type="arrowright" size="14" color="#4f6ef7" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">备注</view>
|
||||
<view v-if="!editing" class="val">{{ data.notes || '-' }}</view>
|
||||
<input v-else v-model="draft.notes" class="input" placeholder="请输入备注" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view id="anchor-behavior" class="section-title">
|
||||
行为画像
|
||||
</view>
|
||||
<view class="rows">
|
||||
<view class="row">
|
||||
<view class="label">门诊号</view>
|
||||
<view class="val">{{ data.outpatientNo || '-' }}</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">住院号</view>
|
||||
<view class="val">{{ data.inpatientNo || '-' }}</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="label">病案号</view>
|
||||
<view class="val">{{ data.medicalRecordNo || '-' }}</view>
|
||||
<form-template
|
||||
v-if="editing && effectiveInternalItems.length"
|
||||
ref="internalFormRef"
|
||||
:items="effectiveInternalItems"
|
||||
:form="forms"
|
||||
:filterRule="filterRule"
|
||||
@change="onChange"
|
||||
/>
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="item in effectiveInternalItems"
|
||||
:key="item.title"
|
||||
class="row"
|
||||
>
|
||||
<view class="label">{{ item.name || item.title }}</view>
|
||||
<view class="val" :class="displayValue(item) === '-' ? 'muted' : ''">{{ displayValue(item) }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -120,11 +115,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FormTemplate from '@/components/form-template/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Object, default: () => ({}) },
|
||||
baseItems: { type: Array, default: () => ([]) },
|
||||
internalItems: { type: Array, default: () => ([]) },
|
||||
floatingBottom: { type: Number, default: 16 },
|
||||
});
|
||||
const emit = defineEmits(['save']);
|
||||
@ -132,60 +130,74 @@ const emit = defineEmits(['save']);
|
||||
const anchors = [
|
||||
{ label: '基本信息', value: 'base' },
|
||||
{ label: '内部信息', value: 'internal' },
|
||||
{ label: '行为画像', value: 'behavior' },
|
||||
];
|
||||
const activeAnchor = ref('base');
|
||||
|
||||
const editing = ref(false);
|
||||
const sexOptions = ['男', '女'];
|
||||
const draft = reactive({
|
||||
name: '',
|
||||
sex: '',
|
||||
age: '',
|
||||
mobile: '',
|
||||
notes: '',
|
||||
});
|
||||
const baseFormRef = ref(null);
|
||||
const internalFormRef = ref(null);
|
||||
const patch = reactive({});
|
||||
const forms = computed(() => ({ ...(props.data || {}), ...patch }));
|
||||
|
||||
const fallbackBaseItems = [
|
||||
{ title: 'name', name: '姓名', type: 'input', operateType: 'formCell', required: true, wordLimit: 20 },
|
||||
{ title: 'sex', name: '性别', type: 'select', operateType: 'formCell', required: false, range: ['男', '女'] },
|
||||
{ title: 'age', name: '年龄', type: 'input', inputType: 'number', operateType: 'formCell', required: false, wordLimit: 3 },
|
||||
{ title: 'mobile', name: '联系电话', type: 'input', inputType: 'number', operateType: 'formCell', required: false, wordLimit: 11 },
|
||||
];
|
||||
|
||||
const fallbackInternalItems = [
|
||||
{ title: 'notes', name: '备注', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
|
||||
];
|
||||
|
||||
const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems));
|
||||
const effectiveInternalItems = computed(() => (Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems));
|
||||
|
||||
const filterRule = {
|
||||
reference(formModel) {
|
||||
const customerSource = Array.isArray(formModel.customerSource)
|
||||
? formModel.customerSource
|
||||
: typeof formModel.customerSource === 'string'
|
||||
? [formModel.customerSource]
|
||||
: [];
|
||||
return ['同事推荐', '客户推荐'].includes(customerSource[0]) && customerSource.length === 1;
|
||||
},
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!editing.value) syncDraft();
|
||||
if (!editing.value) Object.keys(patch).forEach((k) => delete patch[k]);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function syncDraft() {
|
||||
draft.name = props.data?.name || '';
|
||||
draft.sex = props.data?.sex || '';
|
||||
draft.age = props.data?.age || '';
|
||||
draft.mobile = props.data?.mobile || '';
|
||||
draft.notes = props.data?.notes || '';
|
||||
function normalizeChangeValue(value) {
|
||||
if (value && typeof value === 'object' && 'value' in value) return value.value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
if (editing.value) return;
|
||||
editing.value = true;
|
||||
syncDraft();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editing.value = false;
|
||||
Object.keys(patch).forEach((k) => delete patch[k]);
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', {
|
||||
name: draft.name,
|
||||
sex: draft.sex,
|
||||
age: draft.age,
|
||||
mobile: draft.mobile,
|
||||
notes: draft.notes,
|
||||
});
|
||||
if (baseFormRef.value?.verify && !baseFormRef.value.verify()) return;
|
||||
if (internalFormRef.value?.verify && !internalFormRef.value.verify()) return;
|
||||
const out = { ...patch };
|
||||
emit('save', out);
|
||||
editing.value = false;
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
Object.keys(patch).forEach((k) => delete patch[k]);
|
||||
}
|
||||
|
||||
function pickSex(e) {
|
||||
draft.sex = sexOptions[e.detail.value] || '';
|
||||
function onChange({ title, value }) {
|
||||
patch[title] = normalizeChangeValue(value);
|
||||
}
|
||||
|
||||
function call(mobile) {
|
||||
@ -193,6 +205,43 @@ function call(mobile) {
|
||||
uni.makePhoneCall({ phoneNumber: String(mobile) });
|
||||
}
|
||||
|
||||
function rawValue(item) {
|
||||
const title = item?.title;
|
||||
if (!title) return '';
|
||||
const v = forms.value?.[title];
|
||||
return v === undefined || v === null ? '' : v;
|
||||
}
|
||||
|
||||
function displayValue(item) {
|
||||
const title = item?.title;
|
||||
const type = item?.type;
|
||||
if (!title) return '-';
|
||||
const v = forms.value?.[title];
|
||||
if (v === undefined || v === null || v === '') return '-';
|
||||
|
||||
if (type === 'files') {
|
||||
const list = Array.isArray(v) ? v : typeof v === 'string' && v ? [v] : [];
|
||||
return list.length ? `已上传${list.length}项` : '-';
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) return v.filter(Boolean).join('、') || '-';
|
||||
if (typeof v === 'object') {
|
||||
if ('label' in v) return String(v.label || '-');
|
||||
if ('name' in v) return String(v.name || '-');
|
||||
if ('value' in v) return String(v.value || '-');
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (type === 'select' || type === 'radio' || type === 'selectAndImage') {
|
||||
const range = Array.isArray(item?.range) ? item.range : [];
|
||||
if (range.length && typeof range[0] === 'object') {
|
||||
const found = range.find((i) => String(i?.value) === String(v));
|
||||
if (found) return String(found.label || found.value || '-');
|
||||
}
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function scrollToAnchor(key) {
|
||||
activeAnchor.value = key;
|
||||
const selector = `#anchor-${key}`;
|
||||
@ -407,4 +456,3 @@ function closeTransferRecord() {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
144
components/form-template/form-cell/form-files.vue
Normal file
144
components/form-template/form-cell/form-files.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<view class="files-wrap">
|
||||
<view class="files-label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
||||
<view class="grid">
|
||||
<view v-for="(f, idx) in files" :key="idx" class="item" @click="preview(idx)">
|
||||
<image class="thumb" :src="f.url" mode="aspectFill" />
|
||||
<view class="remove" @click.stop="remove(idx)">×</view>
|
||||
</view>
|
||||
<view class="add" @click="add">
|
||||
<view class="plus">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { chooseAndUploadImage } from '@/utils/file';
|
||||
import { toast } from '@/utils/widget';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
name: { default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
max: { type: [Number, String], default: 9 },
|
||||
});
|
||||
|
||||
const maxCount = computed(() => {
|
||||
const n = Number(props.max);
|
||||
return Number.isFinite(n) && n > 0 ? Math.min(n, 9) : 9;
|
||||
});
|
||||
|
||||
const files = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
if (Array.isArray(v)) {
|
||||
return v
|
||||
.map((i) => {
|
||||
if (typeof i === 'string') return { url: i };
|
||||
if (i && typeof i === 'object' && i.url) return { url: String(i.url) };
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (typeof v === 'string' && v) return [{ url: v }];
|
||||
return [];
|
||||
});
|
||||
|
||||
function emitValue(list) {
|
||||
emits('change', { title: props.title, value: list });
|
||||
}
|
||||
|
||||
function preview(idx) {
|
||||
const urls = files.value.map((i) => i.url);
|
||||
if (!urls.length) return;
|
||||
uni.previewImage({ urls, current: urls[idx] });
|
||||
}
|
||||
|
||||
function remove(idx) {
|
||||
if (props.disableChange) return;
|
||||
const next = files.value.filter((_, i) => i !== idx);
|
||||
emitValue(next);
|
||||
}
|
||||
|
||||
async function add() {
|
||||
if (props.disableChange) return;
|
||||
if (files.value.length >= maxCount.value) {
|
||||
toast(`最多上传${maxCount.value}张`);
|
||||
return;
|
||||
}
|
||||
const url = await chooseAndUploadImage({ count: 1 });
|
||||
if (!url) return;
|
||||
emitValue([...files.value, { url }]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../cell-style.css';
|
||||
|
||||
.files-wrap {
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.files-label {
|
||||
font-size: 28rpx;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 18rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
border-radius: 18rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 10rpx;
|
||||
border: 1px dashed #cfcfcf;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plus {
|
||||
font-size: 56rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<common-cell :name="name" :required="required">
|
||||
<view class="form-content__wrapper">
|
||||
<input :disabled="disableChange" :value="value" :text="inputType" class="form-input" :placeholder="placeholder"
|
||||
<input :disabled="disableChange" :value="value" :type="inputType" class="form-input" :placeholder="placeholder"
|
||||
placeholder-class="form__placeholder" :maxlength="wordLimit" @input="change($event)" />
|
||||
<view v-if="appendText" class="appendText"> {{ appendText }}</view>
|
||||
</view>
|
||||
@ -45,7 +45,10 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const placeholder = computed(() => `请输入${props.name || ''}`)
|
||||
const value = computed(() => props.form && props.form && props.form[props.title] ? props.form[props.title] : '')
|
||||
const value = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
})
|
||||
|
||||
function change(e) {
|
||||
emits('change', {
|
||||
|
||||
168
components/form-template/form-cell/form-multiSelectAndOther.vue
Normal file
168
components/form-template/form-cell/form-multiSelectAndOther.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<view class="multi-wrap">
|
||||
<view class="label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
||||
<view class="options" :class="hasOtherSelected ? 'with-other' : ''">
|
||||
<view
|
||||
v-for="opt in displayOptions"
|
||||
:key="opt.value"
|
||||
class="opt"
|
||||
:class="isSelected(opt.value) ? 'active' : ''"
|
||||
@click="toggle(opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hasOtherSelected" class="other">
|
||||
<input
|
||||
:disabled="disableChange"
|
||||
:value="otherText"
|
||||
class="other-input"
|
||||
placeholder="请补充其它内容"
|
||||
placeholder-class="form__placeholder"
|
||||
maxlength="50"
|
||||
@input="onOtherInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
range: { type: Array, default: () => [] },
|
||||
name: { default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
otherList: { type: Array, default: () => ['其他'] },
|
||||
});
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter(Boolean).map((i) => ({ label: String(i), value: String(i) }));
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const displayOptions = computed(() => normalizeOptions(props.range));
|
||||
const valueList = computed(() => (Array.isArray(props.form?.[props.title]) ? props.form[props.title] : []));
|
||||
|
||||
const otherValue = computed(() => {
|
||||
const selected = valueList.value.map(String);
|
||||
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||||
const extra = selected.filter((i) => !rangeValues.has(String(i)));
|
||||
return extra[0] || '';
|
||||
});
|
||||
|
||||
const hasOtherSelected = computed(() => {
|
||||
const selected = valueList.value.map(String);
|
||||
return selected.some((v) => props.otherList.map(String).includes(v));
|
||||
});
|
||||
|
||||
const otherText = computed(() => otherValue.value);
|
||||
|
||||
function isSelected(v) {
|
||||
return valueList.value.map(String).includes(String(v));
|
||||
}
|
||||
|
||||
function emitValue(next) {
|
||||
emits('change', { title: props.title, value: next });
|
||||
}
|
||||
|
||||
function toggle(v) {
|
||||
if (props.disableChange) return;
|
||||
const str = String(v);
|
||||
let next = valueList.value.map(String);
|
||||
|
||||
if (next.includes(str)) {
|
||||
next = next.filter((i) => i !== str);
|
||||
} else {
|
||||
next.push(str);
|
||||
}
|
||||
|
||||
const isOther = props.otherList.map(String).includes(str);
|
||||
if (!isOther) {
|
||||
// 如果取消了 other 的选择,清理 other 文本
|
||||
const stillHasOther = next.some((i) => props.otherList.map(String).includes(i));
|
||||
if (!stillHasOther) {
|
||||
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||||
next = next.filter((i) => rangeValues.has(i));
|
||||
}
|
||||
}
|
||||
emitValue(next);
|
||||
}
|
||||
|
||||
function onOtherInput(e) {
|
||||
const text = String(e?.detail?.value || '').trim();
|
||||
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||||
let next = valueList.value.map(String).filter((i) => rangeValues.has(i));
|
||||
const hasOther = valueList.value.map(String).some((i) => props.otherList.map(String).includes(i));
|
||||
if (hasOther) {
|
||||
next.push(...props.otherList.map(String).slice(0, 1));
|
||||
if (text) next.push(text);
|
||||
}
|
||||
emitValue(next);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../cell-style.css';
|
||||
|
||||
.multi-wrap {
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: 18rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.opt {
|
||||
min-width: 150rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.opt.active {
|
||||
background: rgba(93, 138, 255, 0.12);
|
||||
border-color: rgba(93, 138, 255, 0.35);
|
||||
color: #5d8aff;
|
||||
}
|
||||
|
||||
.other {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
89
components/form-template/form-cell/form-run-time.vue
Normal file
89
components/form-template/form-cell/form-run-time.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<view class="form-row">
|
||||
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
||||
<view class="form-row__content runtime">
|
||||
<input :disabled="disableChange" class="num" type="number" :value="year" @input="onInput($event, 'year')" />
|
||||
<text class="unit">年</text>
|
||||
<input :disabled="disableChange" class="num" type="number" :value="month" @input="onInput($event, 'month')" />
|
||||
<text class="unit">月</text>
|
||||
<input :disabled="disableChange" class="num" type="number" :value="day" @input="onInput($event, 'day')" />
|
||||
<text class="unit">日</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
name: { default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const raw = computed(() => (props.form && props.form[props.title] ? String(props.form[props.title]) : ''));
|
||||
const year = ref('');
|
||||
const month = ref('');
|
||||
const day = ref('');
|
||||
|
||||
watch(
|
||||
raw,
|
||||
(n) => {
|
||||
if (!n) {
|
||||
year.value = '';
|
||||
month.value = '';
|
||||
day.value = '';
|
||||
return;
|
||||
}
|
||||
const yearMatch = n.match(/(\d+)年/);
|
||||
const monthMatch = n.match(/(\d+)月/);
|
||||
const dayMatch = n.match(/(\d+)日/);
|
||||
year.value = yearMatch ? yearMatch[1] : '';
|
||||
month.value = monthMatch ? monthMatch[1] : '';
|
||||
day.value = dayMatch ? dayMatch[1] : '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function build() {
|
||||
const y = year.value && year.value !== '0' ? `${year.value}年` : '';
|
||||
const m = month.value && month.value !== '0' ? `${month.value}月` : '';
|
||||
const d = day.value && day.value !== '0' ? `${day.value}日` : '';
|
||||
emits('change', { title: props.title, value: `${y}${m}${d}` });
|
||||
}
|
||||
|
||||
function onInput(e, key) {
|
||||
const v = String(e?.detail?.value || '').replace(/[^\d]/g, '');
|
||||
if (key === 'year') year.value = v;
|
||||
if (key === 'month') month.value = v;
|
||||
if (key === 'day') day.value = v;
|
||||
build();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../cell-style.css';
|
||||
|
||||
.runtime {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.num {
|
||||
width: 80rpx;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
65
components/form-template/form-cell/form-select-image.vue
Normal file
65
components/form-template/form-cell/form-select-image.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<picker mode="selector" :range="displayRange" :disabled="disableChange" @change="change($event)">
|
||||
<common-cell :name="name" :required="required">
|
||||
<view class="form-content__wrapper">
|
||||
<view class="flex-main-content truncate" :class="valueLabel ? '' : 'form__placeholder'">
|
||||
{{ valueLabel || placeholder }}
|
||||
</view>
|
||||
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
|
||||
</view>
|
||||
</common-cell>
|
||||
</picker>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import commonCell from '../common-cell.vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
name: { default: '' },
|
||||
range: { type: Array, default: () => [] }, // 支持 string[] 或 {label,value,image}[]
|
||||
required: { type: Boolean, default: false },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
});
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter(Boolean).map((i) => ({ label: String(i), value: String(i) }));
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const options = computed(() => normalizeOptions(props.range));
|
||||
const placeholder = computed(() => `请选择${props.name || ''}`);
|
||||
const displayRange = computed(() => options.value.map((i) => i.label));
|
||||
|
||||
const rawValue = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
});
|
||||
const valueLabel = computed(() => {
|
||||
if (!rawValue.value) return '';
|
||||
const found = options.value.find((i) => String(i.value) === rawValue.value);
|
||||
return found ? found.label : rawValue.value;
|
||||
});
|
||||
|
||||
function change(e) {
|
||||
const idx = Number(e?.detail?.value || 0);
|
||||
const picked = options.value[idx];
|
||||
emits('change', { title: props.title, value: picked ? picked.value : '' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../cell-style.css';
|
||||
</style>
|
||||
90
components/form-template/form-cell/form-select-mobile.vue
Normal file
90
components/form-template/form-cell/form-select-mobile.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="form-row">
|
||||
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
||||
<view class="form-row__content content">
|
||||
<input
|
||||
:disabled="disableChange"
|
||||
:value="value"
|
||||
class="input"
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="form__placeholder"
|
||||
@input="onInput"
|
||||
/>
|
||||
<picker mode="selector" :disabled="disableChange" :range="relationRange" @change="onPick">
|
||||
<view class="relation" :class="noteValue ? '' : 'form__placeholder'">
|
||||
{{ noteValue || '请选择' }}
|
||||
<uni-icons class="arrow" type="arrowdown" size="16" color="#999" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
name: { default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const placeholder = computed(() => `请输入${props.name || ''}`);
|
||||
|
||||
const relationRange = ['本人', '父亲', '母亲', '配偶', '子女', '其他'];
|
||||
const noteTitle = computed(() => `${props.title || ''}Note`);
|
||||
const value = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
});
|
||||
const noteValue = computed(() => {
|
||||
const v = props.form?.[noteTitle.value];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
});
|
||||
|
||||
function onInput(e) {
|
||||
emits('change', { title: props.title, value: String(e?.detail?.value || '') });
|
||||
}
|
||||
|
||||
function onPick(e) {
|
||||
const idx = Number(e?.detail?.value || 0);
|
||||
emits('change', { title: noteTitle.value, value: relationRange[idx] || '' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../cell-style.css';
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.relation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8rpx;
|
||||
min-width: 180rpx;
|
||||
border-left: 1px solid #eee;
|
||||
padding-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
121
components/form-template/form-cell/form-select-other.vue
Normal file
121
components/form-template/form-cell/form-select-other.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view>
|
||||
<picker mode="selector" :range="displayRange" :disabled="disableChange" @change="onPick">
|
||||
<common-cell :name="name" :required="required">
|
||||
<view class="form-content__wrapper">
|
||||
<view class="flex-main-content truncate" :class="selectLabel ? '' : 'form__placeholder'">
|
||||
{{ selectLabel || placeholder }}
|
||||
</view>
|
||||
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
|
||||
</view>
|
||||
</common-cell>
|
||||
</picker>
|
||||
|
||||
<view v-if="showOther" class="other-row">
|
||||
<input
|
||||
:disabled="disableChange"
|
||||
:value="otherValue"
|
||||
class="other-input"
|
||||
placeholder="请输入补充内容"
|
||||
placeholder-class="form__placeholder"
|
||||
maxlength="50"
|
||||
@input="onOtherInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import commonCell from '../common-cell.vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
name: { default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
title: { default: '' },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
range: { type: Array, default: () => [] },
|
||||
otherFiled: { type: String, default: '其他' },
|
||||
});
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter(Boolean).map((i) => ({ label: String(i), value: String(i) }));
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const options = computed(() => normalizeOptions(props.range));
|
||||
const displayRange = computed(() => options.value.map((i) => i.label));
|
||||
const placeholder = computed(() => `请选择${props.name || ''}`);
|
||||
|
||||
const rawValue = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
});
|
||||
|
||||
const isOther = computed(() => {
|
||||
const v = String(rawValue.value || '');
|
||||
if (!v) return false;
|
||||
const known = new Set(options.value.map((i) => String(i.value)));
|
||||
return !known.has(v);
|
||||
});
|
||||
|
||||
const showOther = computed(() => {
|
||||
const selected = selectLabel.value;
|
||||
return selected === props.otherFiled || isOther.value;
|
||||
});
|
||||
|
||||
const selectLabel = computed(() => {
|
||||
const v = String(rawValue.value || '');
|
||||
if (!v) return '';
|
||||
const found = options.value.find((i) => String(i.value) === v);
|
||||
if (found) return found.label;
|
||||
return props.otherFiled;
|
||||
});
|
||||
|
||||
const otherValue = computed(() => (isOther.value ? String(rawValue.value || '') : ''));
|
||||
|
||||
function onPick(e) {
|
||||
const idx = Number(e?.detail?.value || 0);
|
||||
const picked = options.value[idx];
|
||||
if (!picked) return;
|
||||
if (picked.label === props.otherFiled || String(picked.value) === String(props.otherFiled)) {
|
||||
emits('change', { title: props.title, value: '' });
|
||||
return;
|
||||
}
|
||||
emits('change', { title: props.title, value: picked.value });
|
||||
}
|
||||
|
||||
function onOtherInput(e) {
|
||||
emits('change', { title: props.title, value: String(e?.detail?.value || '') });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../cell-style.css';
|
||||
|
||||
.other-row {
|
||||
padding: 0 30rpx 24rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@ -50,9 +50,9 @@ const displayRange = computed(() => {
|
||||
return props.range;
|
||||
})
|
||||
const value = computed(() => {
|
||||
if (!props.form || !props.form[props.title]) return '';
|
||||
|
||||
if (!props.form) return '';
|
||||
const currentValue = props.form[props.title];
|
||||
if (currentValue === undefined || currentValue === null || currentValue === '') return '';
|
||||
// 如果range是对象数组,找到对应的label显示
|
||||
if (Array.isArray(props.range) && props.range.length > 0 && typeof props.range[0] === 'object') {
|
||||
const option = props.range.find(item => item.value === currentValue);
|
||||
|
||||
35
components/form-template/form-cell/form-surgical-history.vue
Normal file
35
components/form-template/form-cell/form-surgical-history.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<form-multi-select-and-other
|
||||
v-bind="attrs"
|
||||
:range="normalizedRange"
|
||||
:form="form"
|
||||
:disableChange="disableChange"
|
||||
:otherList="['有']"
|
||||
@change="change"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import formMultiSelectAndOther from './form-multiSelectAndOther.vue';
|
||||
|
||||
const attrs = useAttrs();
|
||||
const emits = defineEmits(['change']);
|
||||
defineProps({
|
||||
form: { type: Object, default: () => ({}) },
|
||||
disableChange: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const normalizedRange = computed(() => {
|
||||
const r = attrs?.range;
|
||||
if (Array.isArray(r) && r.length) return r;
|
||||
return ['无', '有'];
|
||||
});
|
||||
|
||||
function change(data) {
|
||||
emits('change', data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@ -42,10 +42,13 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const placeholder = computed(() => `请输入${props.name || ''}`)
|
||||
const value = computed(() => props.form && props.form && props.form[props.title] ? props.form[props.title] : '')
|
||||
const value = computed(() => {
|
||||
const v = props.form?.[props.title];
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
})
|
||||
const wordLimit = computed(() => {
|
||||
if (typeof props.wordLimit === 'string' && Number(props.wordLimit) > 0) {
|
||||
return Number.ceil(props.wordLimit)
|
||||
return Math.ceil(Number(props.wordLimit))
|
||||
}
|
||||
if (typeof props.wordLimit === 'number' && props.wordLimit > 0) {
|
||||
return props.wordLimit
|
||||
|
||||
@ -1,11 +1,33 @@
|
||||
<template>
|
||||
<form-datepicker v-if="attrs.type === 'date'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-surgical-history
|
||||
v-if="attrs.title === 'surgicalHistory'"
|
||||
v-bind="attrs"
|
||||
:form="form"
|
||||
:disableChange="disableChange"
|
||||
@change="change"
|
||||
/>
|
||||
<form-datepicker v-else-if="attrs.type === 'date'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-input v-else-if="attrs.type === 'input'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-radio v-else-if="attrs.type === 'radio'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-region v-else-if="attrs.type === 'region'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-select v-else-if="attrs.type === 'select'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-select-other v-else-if="attrs.type === 'selectAndOther'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-select-image v-else-if="attrs.type === 'selectAndImage'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-select-mobile v-else-if="attrs.type === 'selectMobile'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-textarea v-else-if="attrs.type === 'textarea'" v-bind="attrs" :form="form" :disableChange="disableChange"
|
||||
@change="change" />
|
||||
<form-multi-select-and-other
|
||||
v-else-if="attrs.type === 'multiSelectAndOther'"
|
||||
v-bind="attrs"
|
||||
:form="form"
|
||||
:disableChange="disableChange"
|
||||
@change="change"
|
||||
/>
|
||||
<form-run-time v-else-if="attrs.type === 'runTime'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<form-files v-else-if="attrs.type === 'files'" v-bind="attrs" :form="form" :disableChange="disableChange" @change="change" />
|
||||
<view v-else-if="attrs.type || attrs.name || attrs.title" class="py-20rpx px-30rpx border-bottom text-28rpx text-gray-500">
|
||||
{{ attrs.name || attrs.title }}(暂不支持:{{ attrs.type }})
|
||||
</view>
|
||||
|
||||
<!--
|
||||
<form-operation v-else-if="attrs.title === 'surgicalHistory'" v-bind="attrs" :form="form" @change="change"
|
||||
@ -25,6 +47,13 @@ import formRadio from './form-radio.vue';
|
||||
import formDatepicker from './form-datepicker.vue';
|
||||
import formRegion from './form-region.vue';
|
||||
import formTextarea from './form-textarea.vue';
|
||||
import formMultiSelectAndOther from './form-multiSelectAndOther.vue';
|
||||
import formSelectOther from './form-select-other.vue';
|
||||
import formSelectMobile from './form-select-mobile.vue';
|
||||
import formRunTime from './form-run-time.vue';
|
||||
import formFiles from './form-files.vue';
|
||||
import formSelectImage from './form-select-image.vue';
|
||||
import formSurgicalHistory from './form-surgical-history.vue';
|
||||
|
||||
defineProps({
|
||||
form: {
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
<template>
|
||||
<template v-for="item in formItems" :key="item.title">
|
||||
<form-cell v-if="item.cellType === 'formCellItem'" v-bind="item" :form="form" :disableChange="disabledMap[item.title]"
|
||||
@change="change" @addRule="addRule" />
|
||||
<custom-cell v-else-if="item.cellType === 'customCellItem'" v-bind="item" :form="form"
|
||||
:readonly="disabledMap[item.title]" @change="change" @addRule="addRule" />
|
||||
<form-cell v-bind="item" :form="form" :disableChange="disabledMap[item.title]" @change="change" />
|
||||
</template>
|
||||
<form-cell />
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, provide, ref } from 'vue';
|
||||
import verifyForm from './verify.js';
|
||||
|
||||
import FormCell from './form-cell/index.vue';
|
||||
// import CustomCell from './custom-cell/index.vue';
|
||||
|
||||
const emits = defineEmits(['change']);
|
||||
const props = defineProps({
|
||||
@ -31,12 +26,13 @@ const props = defineProps({
|
||||
rule: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
filterRule: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const formCellType = ['input', 'select', 'date', 'radio', 'region', 'textarea', 'multiSelectAndOther', 'selfMultipleDiseases'];
|
||||
const formCellTitle = ['surgicalHistory'];
|
||||
const customCellType = ['BMI', 'selfMultipleDiseases', 'bloodPressure', 'diagnosis'];
|
||||
const disabledMap = computed(() => props.disableTitles.reduce((m, i) => {
|
||||
if (typeof i === 'string' && i.trim()) {
|
||||
m[i] = true;
|
||||
@ -49,22 +45,30 @@ provide('addRule', addRule);
|
||||
const customRule = ref({});
|
||||
|
||||
const formItems = computed(() => {
|
||||
return props.items.map(i => {
|
||||
let cellType = '';
|
||||
if (customCellType.includes(i.type)) {
|
||||
cellType = 'customCellItem';
|
||||
} else if (formCellType.includes(i.type) || formCellTitle.includes(i.title)) {
|
||||
cellType = 'formCellItem';
|
||||
}
|
||||
return { ...i, cellType }
|
||||
return props.items
|
||||
.filter((i) => {
|
||||
if (!i) return false;
|
||||
const fn = props.filterRule && typeof props.filterRule[i.title] === 'function' ? props.filterRule[i.title] : null;
|
||||
return fn ? fn(props.form) : true;
|
||||
})
|
||||
.map((i) => ({ ...i }));
|
||||
})
|
||||
|
||||
const rules = computed(() => ({ ...customRule.value, ...props.rule }));
|
||||
|
||||
function addRule({ title, fn }) {
|
||||
if (title && props.items.some(i => i.title === title) && typeof fn === 'function') {
|
||||
customRule.value[title] = fn;
|
||||
function addRule(arg1, arg2) {
|
||||
// 兼容两种调用方式:addRule({title, fn}) / addRule(title, fn)
|
||||
if (typeof arg1 === 'string' && typeof arg2 === 'function') {
|
||||
const title = arg1;
|
||||
const fn = arg2;
|
||||
if (title && props.items.some((i) => i.title === title)) customRule.value[title] = fn;
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg1 && typeof arg1 === 'object') {
|
||||
const title = arg1.title;
|
||||
const fn = arg1.fn;
|
||||
if (title && props.items.some((i) => i.title === title) && typeof fn === 'function') customRule.value[title] = fn;
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +77,8 @@ function change(data) {
|
||||
}
|
||||
|
||||
function verify() {
|
||||
return verifyForm(props.items, rules.value, props.form)
|
||||
const visible = formItems.value.filter((i) => i && !i.hidden);
|
||||
return verifyForm(visible, rules.value, props.form)
|
||||
}
|
||||
|
||||
defineExpose({ verify })
|
||||
|
||||
@ -8,6 +8,12 @@ const FormRule = {
|
||||
radio: checkListItem,
|
||||
select: checkListItem,
|
||||
textarea: checkInput,
|
||||
selectMobile: checkInput,
|
||||
selectAndOther: checkInput,
|
||||
selectAndImage: checkListItem,
|
||||
multiSelectAndOther: checkMultiList,
|
||||
files: checkFiles,
|
||||
runTime: checkInput,
|
||||
}
|
||||
|
||||
const fixedRule = {
|
||||
@ -41,8 +47,8 @@ export default function verify(items, rule = {}, data = {}) {
|
||||
}
|
||||
|
||||
function checkDate({ name, required }, value) {
|
||||
if (!required && isTrueValue(value)) return true;
|
||||
if (required && !value) {
|
||||
if (!required && !isTrueValue(value)) return true;
|
||||
if (required && !isTrueValue(value)) {
|
||||
toast(`请选择${name}`)
|
||||
return false
|
||||
}
|
||||
@ -55,7 +61,22 @@ function checkDate({ name, required }, value) {
|
||||
|
||||
function checkListItem({ range, name, required }, value) {
|
||||
if (isTrueValue(value)) {
|
||||
if (!Array.isArray(range) || !range.includes(value)) {
|
||||
if (!Array.isArray(range) || range.length === 0) {
|
||||
toast(`${name}所选值无效,请重新选择`)
|
||||
return false
|
||||
}
|
||||
const candidate = typeof value === 'string' || typeof value === 'number' ? String(value) : value;
|
||||
const isObjectRange = range.length > 0 && range[0] && typeof range[0] === 'object';
|
||||
if (isObjectRange) {
|
||||
const values = range
|
||||
.map((i) => (i && typeof i === 'object' && 'value' in i ? i.value : undefined))
|
||||
.filter((i) => i !== undefined && i !== null)
|
||||
.map((i) => String(i));
|
||||
if (!values.includes(String(candidate))) {
|
||||
toast(`${name}所选值无效,请重新选择`)
|
||||
return false
|
||||
}
|
||||
} else if (!range.map((i) => String(i)).includes(String(candidate))) {
|
||||
toast(`${name}所选值无效,请重新选择`)
|
||||
return false
|
||||
}
|
||||
@ -87,7 +108,7 @@ function checkIdCard(value, name) {
|
||||
}
|
||||
|
||||
function checkInput({ name, required, wordLimit, inputType }, value) {
|
||||
if (!required && !value) return true;
|
||||
if (!required && !isTrueValue(value)) return true;
|
||||
if (required && !isTrueValue(value)) {
|
||||
toast(`请输入${name}`)
|
||||
return false
|
||||
@ -99,6 +120,34 @@ function checkInput({ name, required, wordLimit, inputType }, value) {
|
||||
return true
|
||||
}
|
||||
|
||||
function checkMultiList({ name, required }, value) {
|
||||
const list = Array.isArray(value) ? value.filter(isTrueValue) : [];
|
||||
if (required && list.length === 0) {
|
||||
toast(`请选择${name}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkFiles({ name, required }, value) {
|
||||
const list = Array.isArray(value)
|
||||
? value
|
||||
.map((i) => {
|
||||
if (typeof i === 'string') return i;
|
||||
if (i && typeof i === 'object' && i.url) return String(i.url);
|
||||
return '';
|
||||
})
|
||||
.filter((i) => typeof i === 'string' && i.trim() !== '')
|
||||
: typeof value === 'string' && value.trim() !== ''
|
||||
? [value]
|
||||
: [];
|
||||
if (required && list.length === 0) {
|
||||
toast(`请上传${name}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isTrueValue(value) {
|
||||
if (typeof value === 'string') return value.trim() !== '';
|
||||
if (typeof value === 'number') return true;
|
||||
|
||||
@ -219,13 +219,19 @@
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, ref } from 'vue';
|
||||
import { onLoad, onPullDownRefresh, onReachBottom, onReady, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import HealthProfileTab from '@/components/archive-detail/health-profile-tab.vue';
|
||||
import ServiceInfoTab from '@/components/archive-detail/service-info-tab.vue';
|
||||
import FollowUpManageTab from '@/components/archive-detail/follow-up-manage-tab.vue';
|
||||
import { ensureSeed } from '@/components/archive-detail/mock';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||||
|
||||
const STORAGE_KEY = 'ykt_case_archive_detail';
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
||||
|
||||
const tabs = [
|
||||
{ key: 'visitRecord', title: '病历记录' },
|
||||
@ -257,6 +263,110 @@ const archive = ref({
|
||||
groupOptions: ['高血压', '糖尿病', '高血脂']
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
function getUserId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||
}
|
||||
|
||||
function getCorpId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
return String(d.corpId || a.corpId || team.corpId || '') || '';
|
||||
}
|
||||
|
||||
function normalizeArchiveFromApi(raw) {
|
||||
const r = raw && typeof raw === 'object' ? raw : {};
|
||||
const next = {
|
||||
_id: r._id || archiveId.value,
|
||||
name: r.name || '',
|
||||
sex: r.sex || r.gender || '',
|
||||
age: r.age ?? '',
|
||||
avatar: r.avatar || '',
|
||||
mobile: r.mobile || r.phone1 || r.phone || '',
|
||||
outpatientNo: r.outpatientNo || '',
|
||||
inpatientNo: r.inpatientNo || '',
|
||||
medicalRecordNo: r.medicalRecordNo || '',
|
||||
createTime: r.createTime || '',
|
||||
creator: r.creator || '',
|
||||
notes: r.notes || r.remark || '',
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchArchive() {
|
||||
if (!archiveId.value) return;
|
||||
await ensureDoctor();
|
||||
loading('加载中...');
|
||||
try {
|
||||
const res = await api('getCustomerByCustomerId', { customerId: archiveId.value });
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取档案失败');
|
||||
return;
|
||||
}
|
||||
archive.value = { ...archive.value, ...normalizeArchiveFromApi(res.data) };
|
||||
saveToStorage();
|
||||
} catch (e) {
|
||||
toast('获取档案失败');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateArchive(patch) {
|
||||
const id = String(archiveId.value || '');
|
||||
if (!id) return false;
|
||||
await ensureDoctor();
|
||||
const userId = getUserId();
|
||||
const corpId = getCorpId();
|
||||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
const createTeamId = team?.teamId ? String(team.teamId) : '';
|
||||
const createTeamName = team?.name ? String(team.name) : '';
|
||||
if (!userId || !corpId) {
|
||||
toast('缺少登录信息,请先完成登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
loading('保存中...');
|
||||
try {
|
||||
const res = await api('updateCustomer', {
|
||||
id,
|
||||
params: patch,
|
||||
userId,
|
||||
corpId,
|
||||
createTeamId,
|
||||
createTeamName,
|
||||
});
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '保存失败');
|
||||
return false;
|
||||
}
|
||||
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
||||
await fetchArchive();
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast('保存失败');
|
||||
return false;
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
archiveId.value = options?.id ? String(options.id) : String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || '');
|
||||
|
||||
@ -279,6 +389,9 @@ onLoad((options) => {
|
||||
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
|
||||
}
|
||||
ensureSeed(archiveId.value, archive.value);
|
||||
|
||||
// 接口兜底:优先用接口刷新档案详情
|
||||
fetchArchive();
|
||||
});
|
||||
|
||||
function measureTabsTop() {
|
||||
@ -308,6 +421,7 @@ onShow(() => {
|
||||
};
|
||||
}
|
||||
setTimeout(measureTabsTop, 30);
|
||||
fetchArchive();
|
||||
});
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
@ -389,9 +503,7 @@ const saveContact = () => {
|
||||
uni.showToast({ title: '请输入联系电话', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
archive.value.mobile = v;
|
||||
saveToStorage();
|
||||
closeContact();
|
||||
updateArchive({ mobile: v }).then((ok) => ok && closeContact());
|
||||
};
|
||||
|
||||
// ===== 备注 =====
|
||||
@ -408,9 +520,8 @@ const closeNotes = () => {
|
||||
};
|
||||
|
||||
const saveNotes = () => {
|
||||
archive.value.notes = String(notesInput.value || '').trim();
|
||||
saveToStorage();
|
||||
closeNotes();
|
||||
const v = String(notesInput.value || '').trim();
|
||||
updateArchive({ notes: v }).then((ok) => ok && closeNotes());
|
||||
};
|
||||
|
||||
// ===== 分组 =====
|
||||
|
||||
@ -1,16 +1,27 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<CustomerProfileTab :data="archive" :floatingBottom="16" @save="savePatch" />
|
||||
<CustomerProfileTab
|
||||
:data="archive"
|
||||
:baseItems="baseItems"
|
||||
:internalItems="internalItems"
|
||||
:floatingBottom="16"
|
||||
@save="savePatch"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import CustomerProfileTab from '@/components/archive-detail/customer-profile-tab.vue';
|
||||
import { ensureSeed } from '@/components/archive-detail/mock';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||||
|
||||
const STORAGE_KEY = 'ykt_case_archive_detail';
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
||||
|
||||
const archiveId = ref('');
|
||||
const archive = ref({
|
||||
@ -31,6 +42,108 @@ const archive = ref({
|
||||
groupOptions: [],
|
||||
});
|
||||
|
||||
const baseItems = ref([]);
|
||||
const internalItems = ref([]);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter((i) => typeof i === 'string');
|
||||
if (typeof options[0] === 'object') {
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeTemplateItem(item) {
|
||||
const next = { ...(item || {}) };
|
||||
if (next.operateType === 'custom') next.operateType = 'formCell';
|
||||
|
||||
const originalType = next.type;
|
||||
const customTypeMap = {
|
||||
customerSource: 'select',
|
||||
customerStage: 'select',
|
||||
tag: 'multiSelectAndOther',
|
||||
reference: 'input',
|
||||
selectWwuser: 'select',
|
||||
files: 'files',
|
||||
corpProject: 'select',
|
||||
diagnosis: 'textarea',
|
||||
BMI: 'input',
|
||||
bloodPressure: 'textarea',
|
||||
selfMultipleDiseases: 'textarea',
|
||||
};
|
||||
if (originalType && customTypeMap[originalType]) {
|
||||
next.__originType = originalType;
|
||||
next.type = customTypeMap[originalType];
|
||||
}
|
||||
|
||||
const aliasTypeMap = {
|
||||
text: 'input',
|
||||
string: 'input',
|
||||
number: 'input',
|
||||
integer: 'input',
|
||||
int: 'input',
|
||||
};
|
||||
if (next.type && aliasTypeMap[next.type]) {
|
||||
next.type = aliasTypeMap[next.type];
|
||||
if (!next.inputType && (originalType === 'number' || originalType === 'integer' || originalType === 'int')) next.inputType = 'number';
|
||||
}
|
||||
|
||||
const rawRange = next.range || next.options || next.optionList || next.values || [];
|
||||
const range = normalizeOptions(rawRange);
|
||||
if (next.type === 'select' || next.type === 'selectAndOther' || next.type === 'selectAndImage') {
|
||||
next.range = range;
|
||||
} else if (next.type === 'radio') {
|
||||
if (range.length && typeof range[0] === 'object') {
|
||||
next.type = 'select';
|
||||
next.range = range;
|
||||
} else {
|
||||
next.range = Array.isArray(rawRange) ? rawRange : [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!next.operateType) next.operateType = 'formCell';
|
||||
next.required = Boolean(next.required);
|
||||
if (next.type === 'input' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 20;
|
||||
if (next.type === 'textarea' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 200;
|
||||
return next;
|
||||
}
|
||||
|
||||
function getUserId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||
}
|
||||
|
||||
function getCorpId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
return String(d.corpId || a.corpId || team.corpId || '') || '';
|
||||
}
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromStorage() {
|
||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||
if (cached && typeof cached === 'object') {
|
||||
@ -41,23 +154,109 @@ function loadFromStorage() {
|
||||
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions,
|
||||
};
|
||||
}
|
||||
if (!archiveId.value) {
|
||||
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
|
||||
}
|
||||
ensureSeed(archiveId.value, archive.value);
|
||||
}
|
||||
|
||||
function savePatch(patch) {
|
||||
archive.value = { ...archive.value, ...(patch && typeof patch === 'object' ? patch : {}) };
|
||||
async function loadTemplates() {
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
const [baseRes, internalRes] = await Promise.all([
|
||||
api('getCurrentTemplate', { corpId, templateType: 'baseTemplate' }),
|
||||
api('getCurrentTemplate', { corpId, templateType: 'internalTemplate' }),
|
||||
]);
|
||||
|
||||
if (baseRes?.success) {
|
||||
const temp = baseRes?.data && typeof baseRes.data === 'object' ? baseRes.data : baseRes;
|
||||
const list = Array.isArray(temp.templateList) ? temp.templateList : [];
|
||||
baseItems.value = list
|
||||
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||
.filter((i) => i.operateType !== 'onlyRead')
|
||||
.map(normalizeTemplateItem);
|
||||
}
|
||||
|
||||
if (internalRes?.success) {
|
||||
const temp = internalRes?.data && typeof internalRes.data === 'object' ? internalRes.data : internalRes;
|
||||
const list = Array.isArray(temp.templateList) ? temp.templateList : [];
|
||||
internalItems.value = list
|
||||
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||
.filter((i) => i.operateType !== 'onlyRead')
|
||||
.map(normalizeTemplateItem);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadArchiveFromApi() {
|
||||
if (!archiveId.value) return;
|
||||
loading('加载中...');
|
||||
try {
|
||||
const res = await api('getCustomerByCustomerId', { customerId: archiveId.value });
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取档案失败');
|
||||
return;
|
||||
}
|
||||
archive.value = { ...archive.value, ...(res.data && typeof res.data === 'object' ? res.data : {}) };
|
||||
uni.setStorageSync(STORAGE_KEY, { ...archive.value });
|
||||
} catch (e) {
|
||||
toast('获取档案失败');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function savePatch(patch) {
|
||||
const p = patch && typeof patch === 'object' ? patch : {};
|
||||
const params = Object.keys(p).reduce((acc, k) => {
|
||||
const v = p[k];
|
||||
if (v !== undefined) acc[k] = v;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Object.keys(params).length === 0) return;
|
||||
|
||||
await ensureDoctor();
|
||||
const userId = getUserId();
|
||||
const corpId = getCorpId();
|
||||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
const createTeamId = team?.teamId ? String(team.teamId) : '';
|
||||
const createTeamName = team?.name ? String(team.name) : '';
|
||||
if (!archiveId.value || !userId || !corpId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
loading('保存中...');
|
||||
try {
|
||||
const res = await api('updateCustomer', {
|
||||
id: archiveId.value,
|
||||
params,
|
||||
userId,
|
||||
corpId,
|
||||
createTeamId,
|
||||
createTeamName,
|
||||
});
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '保存失败');
|
||||
return;
|
||||
}
|
||||
toast('保存成功');
|
||||
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
||||
await loadArchiveFromApi();
|
||||
} catch (e) {
|
||||
toast('保存失败');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||
loadFromStorage();
|
||||
ensureDoctor().then(async () => {
|
||||
await Promise.all([loadTemplates(), loadArchiveFromApi()]);
|
||||
});
|
||||
});
|
||||
|
||||
onShow(() => loadFromStorage());
|
||||
onShow(() => {
|
||||
loadFromStorage();
|
||||
loadArchiveFromApi();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -52,17 +52,19 @@
|
||||
class="patient-list"
|
||||
:scroll-into-view="scrollIntoId"
|
||||
:scroll-with-animation="true"
|
||||
lower-threshold="80"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-for="group in patientList" :key="group.letter" :id="'letter-' + group.letter">
|
||||
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
|
||||
<view class="group-title">{{ group.letter }}</view>
|
||||
|
||||
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="handlePatientClick(patient)">
|
||||
<!-- Checkbox for Batch Mode -->
|
||||
<view v-if="isBatchMode" class="checkbox-area">
|
||||
<uni-icons
|
||||
:type="selectedItems.includes(patient.phone) ? 'checkbox-filled' : 'checkbox'"
|
||||
:type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'"
|
||||
size="24"
|
||||
:color="selectedItems.includes(patient.phone) ? '#007aff' : '#ccc'"
|
||||
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"
|
||||
></uni-icons>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
@ -133,10 +135,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { toast } from '@/utils/widget';
|
||||
|
||||
// State
|
||||
const currentTeam = ref('张敏西服务团队');
|
||||
const teams = ref([]);
|
||||
const currentTeam = ref(null);
|
||||
const currentTab = ref(0);
|
||||
const scrollIntoId = ref('');
|
||||
const tabs = ['全部', '新患者', '糖尿病', '高血压', '冠心病', '慢阻肺'];
|
||||
@ -144,85 +154,230 @@ const isBatchMode = ref(false);
|
||||
const selectedItems = ref([]); // Stores patient phone or unique ID
|
||||
|
||||
// 新增流程所需状态(后续接接口替换)
|
||||
const managedArchiveCountAllTeams = ref(10); // 在管档案数(所有团队)
|
||||
const managedArchiveCountAllTeams = ref(0); // 在管档案数(所有团队)
|
||||
const isVerified = ref(true); // 是否已认证
|
||||
const hasVerifyFailedHistory = ref(false); // 是否有历史认证失败
|
||||
const verifyFailedReason = ref('资料不完整,请补充营业执照/资质证明后重新提交。');
|
||||
|
||||
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
||||
|
||||
const teamDisplay = computed(() => `${currentTeam.value}(${managedArchiveCountAllTeams.value})`);
|
||||
const page = ref(1);
|
||||
const pages = ref(0);
|
||||
const pageSize = ref(50);
|
||||
const totalFromApi = ref(0);
|
||||
const loading = ref(false);
|
||||
const rawPatients = ref([]);
|
||||
const more = computed(() => page.value < pages.value);
|
||||
|
||||
// Mock Data
|
||||
const allPatients = [
|
||||
{
|
||||
letter: 'A',
|
||||
data: [
|
||||
{
|
||||
name: '安乐', gender: '男', age: 45, tags: ['糖尿病'],
|
||||
record: { type: '门诊', date: '2026.1.10', diagnosis: '2型糖尿病' },
|
||||
createTime: '2026.1.19 14:30', creator: '李医生', phone: '13888888888', hospitalId: '1001',
|
||||
outpatientNo: '2828393893', inpatientNo: '', medicalRecordNo: '',
|
||||
createdByDoctor: true, hasBindWechat: false
|
||||
},
|
||||
{
|
||||
name: '奥利奥', gender: '女', age: 22, tags: [], record: null,
|
||||
createTime: '2026.1.15 09:00', creator: '王医生', phone: '13999999999', hospitalId: '1002',
|
||||
outpatientNo: '', inpatientNo: '', medicalRecordNo: '',
|
||||
createdByDoctor: false, hasBindWechat: true
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}(${managedArchiveCountAllTeams.value})`);
|
||||
|
||||
function asArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function normalizeTeam(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const teamId = raw.teamId || raw.id || raw._id || '';
|
||||
const name = raw.name || raw.teamName || raw.team || '';
|
||||
const corpId = raw.corpId || raw.corpID || '';
|
||||
const userId = raw.userId || raw.userid || raw.corpUserId || '';
|
||||
if (!teamId || !name) return null;
|
||||
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '', userId: userId ? String(userId) : '' };
|
||||
}
|
||||
|
||||
function getUserId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||
}
|
||||
|
||||
function getCorpId() {
|
||||
const t = currentTeam.value || {};
|
||||
const a = account.value || {};
|
||||
return String(t.corpId || a.corpId || '') || '';
|
||||
}
|
||||
|
||||
function getTeamId() {
|
||||
return String(currentTeam.value?.teamId || '') || '';
|
||||
}
|
||||
|
||||
function getLetter(patient) {
|
||||
const raw = patient?.firstLetter || patient?.nameFirstLetter || patient?.pinyinFirstLetter || patient?.letter || '';
|
||||
const candidate = String(raw || '').trim();
|
||||
if (candidate && /^[A-Za-z]$/.test(candidate)) return candidate.toUpperCase();
|
||||
const name = String(patient?.name || '').trim();
|
||||
const first = name ? name[0] : '';
|
||||
if (/^[A-Za-z]$/.test(first)) return first.toUpperCase();
|
||||
return '#';
|
||||
}
|
||||
|
||||
function letterToDomId(letter) {
|
||||
return `letter-${letter === '#' ? 'HASH' : letter}`;
|
||||
}
|
||||
|
||||
function getSelectId(patient) {
|
||||
return patient?._id || patient?.id || patient?.phone || patient?.mobile || '';
|
||||
}
|
||||
|
||||
function parseCreateTime(value) {
|
||||
if (!value) return null;
|
||||
if (typeof value === 'number') return dayjs(value);
|
||||
if (typeof value === 'string' && /^\d{10,13}$/.test(value)) {
|
||||
const n = Number(value);
|
||||
const ms = value.length === 10 ? n * 1000 : n;
|
||||
return dayjs(ms);
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
letter: 'L',
|
||||
data: [
|
||||
{
|
||||
name: '李珊珊', gender: '女', age: 37, tags: ['糖尿病', '高血压'],
|
||||
record: { type: '门诊', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
|
||||
createTime: '2026.1.10 10:20', creator: '张医生', phone: '13666666666', hospitalId: '1003',
|
||||
outpatientNo: '2828393893', inpatientNo: '2828393893', medicalRecordNo: '2828393893',
|
||||
createdByDoctor: true, hasBindWechat: false
|
||||
},
|
||||
{
|
||||
name: '李珊珊', gender: '女', age: 37, tags: [],
|
||||
record: { type: '住院', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
|
||||
createTime: '2025.12.30 11:00', creator: '张医生', phone: '13666666667', hospitalId: '10031',
|
||||
outpatientNo: '', inpatientNo: '2828393893', medicalRecordNo: '',
|
||||
createdByDoctor: false, hasBindWechat: true
|
||||
},
|
||||
{
|
||||
name: '李某某', gender: '女', age: 37, tags: [], record: null,
|
||||
createTime: '2025.12.01 08:30', creator: '系统导入', phone: '13555555555', hospitalId: '1004',
|
||||
outpatientNo: '', inpatientNo: '', medicalRecordNo: '',
|
||||
createdByDoctor: false, hasBindWechat: false
|
||||
},
|
||||
{
|
||||
name: '李四', gender: '男', age: 50, tags: ['高血压'], record: null,
|
||||
createTime: '2026.1.18 16:45', creator: '管理员', phone: '13444444444', hospitalId: '1005',
|
||||
outpatientNo: '', inpatientNo: '', medicalRecordNo: '',
|
||||
createdByDoctor: true, hasBindWechat: true
|
||||
const asString = String(value);
|
||||
const d1 = dayjs(asString);
|
||||
if (d1.isValid()) return d1;
|
||||
const normalized = asString.replace(/\./g, '-').replace(/\//g, '-');
|
||||
const d2 = dayjs(normalized);
|
||||
return d2.isValid() ? d2 : null;
|
||||
}
|
||||
|
||||
function formatPatient(raw) {
|
||||
const name = raw?.name || raw?.customerName || '';
|
||||
const sex = raw?.sex || raw?.gender || '';
|
||||
const age = raw?.age ?? '';
|
||||
const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean);
|
||||
const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || '');
|
||||
|
||||
const createTime = parseCreateTime(raw?.createTime);
|
||||
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
|
||||
|
||||
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string');
|
||||
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string');
|
||||
|
||||
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
|
||||
|
||||
return {
|
||||
...raw,
|
||||
_id: raw?._id || raw?.id || '',
|
||||
name: String(name || ''),
|
||||
gender: String(sex || ''),
|
||||
age,
|
||||
tags: rawTags.length ? rawTags : (rawTagNames.length ? rawTagNames : tagIds),
|
||||
mobiles,
|
||||
mobile,
|
||||
createTime: createTimeStr,
|
||||
creator: raw?.creatorName || raw?.creator || '',
|
||||
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
|
||||
record: null,
|
||||
createdByDoctor: raw?.addMethod ? String(raw.addMethod) === 'manual' : Boolean(raw?.createdByDoctor),
|
||||
hasBindWechat: Boolean(raw?.externalUserId || raw?.unionid || raw?.hasBindWechat),
|
||||
};
|
||||
}
|
||||
|
||||
function groupByLetter(list) {
|
||||
const map = new Map();
|
||||
list.forEach((item) => {
|
||||
const letter = getLetter(item);
|
||||
const arr = map.get(letter) || [];
|
||||
arr.push(item);
|
||||
map.set(letter, arr);
|
||||
});
|
||||
const letters = Array.from(map.keys()).sort((a, b) => {
|
||||
if (a === '#') return 1;
|
||||
if (b === '#') return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
return letters.map((letter) => ({ letter, data: map.get(letter) || [] }));
|
||||
}
|
||||
|
||||
async function loadTeams() {
|
||||
if (!doctorInfo.value && account.value?.openid) {
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
letter: 'Z',
|
||||
data: [
|
||||
{
|
||||
name: '张三', gender: '男', age: 28, tags: [], record: null,
|
||||
createTime: '2026.1.19 10:00', creator: '赵医生', phone: '13333333333', hospitalId: '1006',
|
||||
outpatientNo: '2828393893', inpatientNo: '', medicalRecordNo: '',
|
||||
createdByDoctor: true, hasBindWechat: false
|
||||
},
|
||||
{
|
||||
name: '张敏', gender: '女', age: 32, tags: ['高血压'],
|
||||
record: { type: '门诊', date: '2025.12.15', diagnosis: '高血压' },
|
||||
createTime: '2025.11.20 15:15', creator: '孙医生', phone: '13222222222', hospitalId: '1007',
|
||||
outpatientNo: '', inpatientNo: '', medicalRecordNo: '2828393893',
|
||||
createdByDoctor: false, hasBindWechat: true
|
||||
}
|
||||
]
|
||||
|
||||
const userId = getUserId();
|
||||
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||
|
||||
if (!corpId || !userId) {
|
||||
toast('缺少用户信息,请先完善个人信息');
|
||||
return;
|
||||
}
|
||||
];
|
||||
|
||||
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取团队失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
||||
const normalized = list.map(normalizeTeam).filter(Boolean);
|
||||
teams.value = normalized;
|
||||
|
||||
const saved = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY);
|
||||
const savedTeamId = saved?.teamId ? String(saved.teamId) : '';
|
||||
currentTeam.value = normalized.find((t) => savedTeamId && t.teamId === savedTeamId) || normalized[0] || null;
|
||||
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
|
||||
}
|
||||
|
||||
async function reload(reset = true) {
|
||||
if (!currentTeam.value) return;
|
||||
if (loading.value) return;
|
||||
|
||||
const userId = getUserId();
|
||||
const corpId = getCorpId();
|
||||
const teamId = getTeamId();
|
||||
if (!corpId || !teamId || !userId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
rawPatients.value = [];
|
||||
pages.value = 0;
|
||||
totalFromApi.value = 0;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const res = await api('searchCorpCustomerWithFollowTime', {
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取患者列表失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload =
|
||||
res && typeof res === 'object'
|
||||
? res.data && typeof res.data === 'object' && !Array.isArray(res.data)
|
||||
? res.data
|
||||
: res
|
||||
: {};
|
||||
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : [];
|
||||
const next = list.map(formatPatient);
|
||||
rawPatients.value = page.value === 1 ? next : [...rawPatients.value, ...next];
|
||||
pages.value = Number(payload.pages || 0) || 0;
|
||||
totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length;
|
||||
managedArchiveCountAllTeams.value =
|
||||
Number(
|
||||
payload.totalAllTeams ||
|
||||
payload.totalAllTeam ||
|
||||
payload.totalAllTeamsCount ||
|
||||
managedArchiveCountAllTeams.value ||
|
||||
totalFromApi.value ||
|
||||
0
|
||||
) || (totalFromApi.value || 0);
|
||||
}
|
||||
|
||||
const handlePatientClick = (patient) => {
|
||||
if (isBatchMode.value) {
|
||||
@ -230,10 +385,14 @@ const handlePatientClick = (patient) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = patient._id || patient.id || patient.mobile || patient.phone || '';
|
||||
uni.setStorageSync(DETAIL_STORAGE_KEY, {
|
||||
_id: id,
|
||||
name: patient.name,
|
||||
sex: patient.gender,
|
||||
age: patient.age,
|
||||
mobile: patient.mobile,
|
||||
mobiles: Array.isArray(patient.mobiles) ? patient.mobiles : (patient.mobile ? [patient.mobile] : []),
|
||||
outpatientNo: patient.outpatientNo,
|
||||
inpatientNo: patient.inpatientNo,
|
||||
medicalRecordNo: patient.medicalRecordNo,
|
||||
@ -242,60 +401,35 @@ const handlePatientClick = (patient) => {
|
||||
createdByDoctor: patient.createdByDoctor,
|
||||
hasBindWechat: patient.hasBindWechat
|
||||
});
|
||||
uni.navigateTo({ url: '/pages/case/archive-detail' });
|
||||
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(String(id))}` });
|
||||
};
|
||||
|
||||
// Computed
|
||||
const patientList = computed(() => {
|
||||
let list = allPatients;
|
||||
const all = rawPatients.value || [];
|
||||
|
||||
// New Patient Filter (Last 7 days)
|
||||
if (currentTab.value === 1) {
|
||||
// Current date logic: 2026-01-20
|
||||
const now = new Date('2026-01-20').getTime();
|
||||
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
let flatList = [];
|
||||
list.forEach(group => {
|
||||
group.data.forEach(p => {
|
||||
if (p.createTime) {
|
||||
// Parse format 2026.1.19 14:30
|
||||
const timeStr = p.createTime.replace(/\./g, '/'); // safari compatibility often needs /, but for calculation standardizing
|
||||
const pTime = new Date(timeStr).getTime();
|
||||
if (pTime >= sevenDaysAgo) {
|
||||
flatList.push({ ...p, _ts: pTime });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by create time desc
|
||||
flatList.sort((a, b) => b._ts - a._ts);
|
||||
|
||||
// Re-group or just show one group?
|
||||
// Usually grouped list maintains grouping or creates a "Search Result" group.
|
||||
// Requirement says "New Patient show...". Usually lists are still grouped by alphabet or just a flat list.
|
||||
// The requirement "Sorted from late to early" for creation time implies order is important, creating letter groups breaks time order.
|
||||
// So for "New Patient" tab, we should probably output a single group or regoup by date?
|
||||
// Requirement 1.2 "New Patient card... order late to early". This strongly implies creating a single list sorted by time.
|
||||
// But the template expects `group.letter` and `group.data`.
|
||||
// I will return a single group named 'Recent' or similar, or just empty letter to hide header.
|
||||
// The template has `<view class="group-title">{{ group.letter }}</view>`. If letter is empty, it shows empty line.
|
||||
// I will return [{ letter: '新患者', data: flatList }]
|
||||
|
||||
const now = dayjs();
|
||||
const sevenDaysAgo = now.subtract(7, 'day').valueOf();
|
||||
const flatList = all
|
||||
.map((p) => {
|
||||
const t = parseCreateTime(p.createTime)?.valueOf();
|
||||
return t ? { ...p, _ts: t } : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((p) => p._ts >= sevenDaysAgo)
|
||||
.sort((a, b) => b._ts - a._ts);
|
||||
return [{ letter: '最近新增', data: flatList }];
|
||||
}
|
||||
|
||||
// Tab Filtering (Mock logic for other tabs)
|
||||
let filtered = all;
|
||||
if (currentTab.value > 1) {
|
||||
const tabName = tabs[currentTab.value];
|
||||
list = list.map(group => ({
|
||||
...group,
|
||||
data: group.data.filter(p => p.tags.includes(tabName))
|
||||
})).filter(group => group.data.length > 0);
|
||||
filtered = filtered.filter((p) => Array.isArray(p.tags) && p.tags.includes(tabName));
|
||||
}
|
||||
|
||||
return list;
|
||||
return groupByLetter(filtered);
|
||||
});
|
||||
|
||||
const indexList = computed(() => {
|
||||
@ -306,6 +440,7 @@ const indexList = computed(() => {
|
||||
const totalPatients = computed(() => {
|
||||
let count = 0;
|
||||
patientList.value.forEach(g => count += g.data.length);
|
||||
if (currentTab.value === 0 && totalFromApi.value) return totalFromApi.value;
|
||||
return count;
|
||||
});
|
||||
|
||||
@ -320,16 +455,21 @@ const checkBatchMode = () => {
|
||||
|
||||
const scrollToLetter = (letter) => {
|
||||
if (currentTab.value === 1) return;
|
||||
scrollIntoId.value = 'letter-' + letter;
|
||||
scrollIntoId.value = letterToDomId(letter);
|
||||
};
|
||||
|
||||
const toggleTeamPopup = () => {
|
||||
if (checkBatchMode()) return;
|
||||
if (!teams.value.length) {
|
||||
toast('暂无可选团队');
|
||||
return;
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList: ['张敏西服务团队', '李医生团队', '王医生团队'],
|
||||
itemList: teams.value.map((i) => i.name),
|
||||
success: function (res) {
|
||||
const teams = ['张敏西服务团队', '李医生团队', '王医生团队'];
|
||||
currentTeam.value = teams[res.tapIndex];
|
||||
currentTeam.value = teams.value[res.tapIndex] || teams.value[0] || null;
|
||||
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
|
||||
reload(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -450,7 +590,7 @@ const openCreatePatientEntry = () => {
|
||||
const toggleSelect = (patient) => {
|
||||
if (!isBatchMode.value) return; // Should not happen if click handler is correct
|
||||
|
||||
const id = patient.phone; // Using phone as unique ID for mock
|
||||
const id = patient._id || patient.id || patient.phone || patient.mobile; // Prefer server id
|
||||
const index = selectedItems.value.indexOf(id);
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1);
|
||||
@ -465,7 +605,7 @@ const handleSelectAll = () => {
|
||||
if (selectedItems.value.length === currentList.length) {
|
||||
selectedItems.value = []; // Unselect All
|
||||
} else {
|
||||
selectedItems.value = currentList.map(p => p.phone);
|
||||
selectedItems.value = currentList.map(p => p._id || p.id || p.phone || p.mobile);
|
||||
}
|
||||
};
|
||||
|
||||
@ -492,6 +632,31 @@ const handleShare = () => {
|
||||
uni.navigateTo({ url: '/pages/case/batch-share' });
|
||||
};
|
||||
|
||||
function loadMore() {
|
||||
if (currentTab.value === 1) return;
|
||||
if (!more.value || loading.value) return;
|
||||
page.value += 1;
|
||||
reload(false);
|
||||
}
|
||||
|
||||
watch(currentTeam, (t) => {
|
||||
if (!t) return;
|
||||
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
|
||||
});
|
||||
|
||||
onLoad(async () => {
|
||||
await loadTeams();
|
||||
if (currentTeam.value) await reload(true);
|
||||
});
|
||||
|
||||
onShow(async () => {
|
||||
const need = uni.getStorageSync(NEED_RELOAD_STORAGE_KEY);
|
||||
if (need) {
|
||||
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
|
||||
await reload(true);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -5,12 +5,23 @@
|
||||
<view class="form-wrap">
|
||||
<form-template ref="formRef" :items="baseItems" :form="form" :rule="rules" @change="onChange" />
|
||||
</view>
|
||||
<view class="scroll-spacer" />
|
||||
</scroll-view>
|
||||
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<cover-view class="footer">
|
||||
<cover-view class="btn plain" @tap="cancel">取消</cover-view>
|
||||
<cover-view class="btn primary" @tap="next">下一步</cover-view>
|
||||
</cover-view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="footer">
|
||||
<button class="primary" @click="next">下一步</button>
|
||||
<button class="btn plain" @click="cancel">取消</button>
|
||||
<button class="btn primary" @click="next">下一步</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -18,36 +29,151 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import dayjs from 'dayjs';
|
||||
import FormTemplate from '@/components/form-template/index.vue';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||||
import validate from '@/utils/validate';
|
||||
|
||||
const STORAGE_KEY = 'patient-create-base';
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
|
||||
const formRef = ref(null);
|
||||
const form = reactive({
|
||||
name: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
mobile: '',
|
||||
birthday: '',
|
||||
idType: '',
|
||||
idNo: ''
|
||||
});
|
||||
const form = reactive({});
|
||||
const baseItems = ref([]);
|
||||
|
||||
const baseItems = [
|
||||
{ title: 'name', name: '姓名', type: 'input', operateType: 'formCell', required: true, wordLimit: 20, inputType: 'text' },
|
||||
{ title: 'gender', name: '性别', type: 'select', operateType: 'formCell', required: false, range: ['男', '女'] },
|
||||
{ title: 'age', name: '年龄', type: 'input', operateType: 'formCell', required: false, wordLimit: 3, inputType: 'number' },
|
||||
{ title: 'mobile', name: '手机号', type: 'input', operateType: 'formCell', required: false, wordLimit: 11, inputType: 'number' },
|
||||
{ title: 'birthday', name: '出生日期', type: 'date', operateType: 'formCell', required: false },
|
||||
{ title: 'idType', name: '证件类型', type: 'select', operateType: 'formCell', required: false, range: ['身份证', '护照', '港澳台通行证', '其他'] },
|
||||
{ title: 'idNo', name: '证件号', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' }
|
||||
];
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
function normalizeChangeValue(value) {
|
||||
if (value && typeof value === 'object' && 'value' in value) return value.value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter((i) => typeof i === 'string');
|
||||
if (typeof options[0] === 'object') {
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeTemplateItem(item) {
|
||||
const next = { ...(item || {}) };
|
||||
|
||||
if (next.operateType === 'custom') next.operateType = 'formCell';
|
||||
|
||||
const originalType = next.type;
|
||||
const customTypeMap = {
|
||||
customerSource: 'select',
|
||||
customerStage: 'select',
|
||||
tag: 'multiSelectAndOther',
|
||||
reference: 'input',
|
||||
selectWwuser: 'select',
|
||||
files: 'files',
|
||||
corpProject: 'select',
|
||||
diagnosis: 'textarea',
|
||||
BMI: 'input',
|
||||
bloodPressure: 'textarea',
|
||||
selfMultipleDiseases: 'textarea',
|
||||
};
|
||||
if (originalType && customTypeMap[originalType]) {
|
||||
next.__originType = originalType;
|
||||
next.type = customTypeMap[originalType];
|
||||
}
|
||||
|
||||
const aliasTypeMap = {
|
||||
text: 'input',
|
||||
string: 'input',
|
||||
number: 'input',
|
||||
integer: 'input',
|
||||
int: 'input',
|
||||
};
|
||||
if (next.type && aliasTypeMap[next.type]) {
|
||||
next.type = aliasTypeMap[next.type];
|
||||
if (!next.inputType && (originalType === 'number' || originalType === 'integer' || originalType === 'int')) next.inputType = 'number';
|
||||
}
|
||||
|
||||
const rawRange = next.range || next.options || next.optionList || next.values || [];
|
||||
const range = normalizeOptions(rawRange);
|
||||
|
||||
if (next.type === 'select' || next.type === 'selectAndOther' || next.type === 'selectAndImage') {
|
||||
next.range = range;
|
||||
} else if (next.type === 'radio') {
|
||||
// wxapp 目前 radio 组件只支持字符串列表;模板如为对象选项则降级为 select
|
||||
if (range.length && typeof range[0] === 'object') {
|
||||
next.type = 'select';
|
||||
next.range = range;
|
||||
} else {
|
||||
next.range = Array.isArray(rawRange) ? rawRange : [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!next.operateType) next.operateType = 'formCell';
|
||||
next.required = Boolean(next.required);
|
||||
|
||||
if (next.type === 'input' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 20;
|
||||
if (next.type === 'textarea' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 200;
|
||||
return next;
|
||||
}
|
||||
|
||||
async function loadBaseTemplate() {
|
||||
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||
if (!corpId) return;
|
||||
|
||||
loading('加载中...');
|
||||
const res = await api('getCurrentTemplate', { corpId, templateType: 'baseTemplate' });
|
||||
hideLoading();
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取模板失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const temp = res?.data && typeof res.data === 'object' ? res.data : res;
|
||||
const list = Array.isArray(temp.templateList) ? temp.templateList : [];
|
||||
baseItems.value = list
|
||||
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||
.filter((i) => i.operateType !== 'onlyRead')
|
||||
.map(normalizeTemplateItem);
|
||||
|
||||
const initialValue = { relationship: '本人', cardType: '身份证' };
|
||||
baseItems.value.forEach((i) => {
|
||||
const title = i?.title;
|
||||
if (!title) return;
|
||||
if (!(title in form) && initialValue[title] !== undefined) {
|
||||
form[title] = initialValue[title];
|
||||
}
|
||||
});
|
||||
|
||||
const cachedTeam = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
if (cachedTeam?.teamId && !form.teamId) form.teamId = String(cachedTeam.teamId);
|
||||
}
|
||||
|
||||
const rules = {
|
||||
idNo(value) {
|
||||
if (!value) return true;
|
||||
if (form.idType === '身份证') {
|
||||
if (form.idType === '身份证' || form.cardType === '身份证') {
|
||||
const [ok, msg] = validate.isChinaId(value);
|
||||
if (!ok) return msg || '证件号格式不正确';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
idCard(value) {
|
||||
if (!value) return true;
|
||||
if (form.cardType === '身份证' || form.idType === '身份证') {
|
||||
const [ok, msg] = validate.isChinaId(value);
|
||||
if (!ok) return msg || '证件号格式不正确';
|
||||
}
|
||||
@ -55,15 +181,31 @@ const rules = {
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
onLoad(async () => {
|
||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||
if (cached && typeof cached === 'object') {
|
||||
Object.assign(form, cached);
|
||||
}
|
||||
|
||||
if (!doctorInfo.value && account.value?.openid) {
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
await loadBaseTemplate();
|
||||
});
|
||||
|
||||
function onChange({ title, value }) {
|
||||
form[title] = value;
|
||||
form[title] = normalizeChangeValue(value);
|
||||
|
||||
if (title === 'idCard' || title === 'idNo') handleIdCardChange(form[title]);
|
||||
else if (title === 'birthday') handleBirthdayChange(form[title]);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function next() {
|
||||
@ -71,22 +213,56 @@ function next() {
|
||||
uni.setStorageSync(STORAGE_KEY, { ...form });
|
||||
uni.navigateTo({ url: '/pages/case/patient-inner-info' });
|
||||
}
|
||||
|
||||
function handleBirthdayChange(value) {
|
||||
if (!value) return;
|
||||
if (!baseItems.value.some((i) => i?.title === 'age')) return;
|
||||
const d = dayjs(String(value));
|
||||
if (!d.isValid()) return;
|
||||
form.age = getAgeFromBirthday(d.format('YYYY-MM-DD'));
|
||||
}
|
||||
|
||||
function handleIdCardChange(value) {
|
||||
if (!value) return;
|
||||
const [ok, birthday, gender] = validate.isChinaId(String(value));
|
||||
if (!ok) return;
|
||||
|
||||
if (baseItems.value.some((i) => i?.title === 'cardType')) form.cardType = '身份证';
|
||||
if (baseItems.value.some((i) => i?.title === 'birthday')) form.birthday = birthday;
|
||||
if (baseItems.value.some((i) => i?.title === 'sex')) form.sex = gender === 'MALE' ? '男' : '女';
|
||||
if (baseItems.value.some((i) => i?.title === 'gender')) form.gender = gender === 'MALE' ? '男' : '女';
|
||||
if (baseItems.value.some((i) => i?.title === 'age')) form.age = getAgeFromBirthday(birthday);
|
||||
}
|
||||
|
||||
function getAgeFromBirthday(birthday) {
|
||||
const d = dayjs(String(birthday));
|
||||
if (!d.isValid()) return '';
|
||||
const now = dayjs();
|
||||
let age = now.year() - d.year();
|
||||
if (now.isBefore(d.add(age, 'year'))) age -= 1;
|
||||
if (age < 0) age = 0;
|
||||
return String(age);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
background: #f6f6f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
@ -113,20 +289,37 @@ function next() {
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.primary {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background: #5d8aff;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
.scroll-spacer {
|
||||
height: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.primary::after {
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn.plain {
|
||||
background: #fff;
|
||||
color: #666;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #5d8aff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,86 +3,283 @@
|
||||
<view class="body">
|
||||
<scroll-view scroll-y class="scroll">
|
||||
<view class="form-wrap">
|
||||
<form-template v-if="items.length" ref="formRef" :items="items" :form="form" :rule="rules" @change="onChange" />
|
||||
<form-template v-if="items.length" ref="formRef" :items="items" :form="forms" :rule="rules" :filterRule="filterRule" @change="onChange" />
|
||||
</view>
|
||||
<view class="scroll-spacer" />
|
||||
</scroll-view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<cover-view class="footer">
|
||||
<cover-view class="btn plain" @tap="prev">上一步</cover-view>
|
||||
<cover-view class="btn primary" @tap="save">保存</cover-view>
|
||||
</cover-view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="footer">
|
||||
<button class="btn plain" @click="cancel">取消</button>
|
||||
<button class="btn primary" @click="save">完成</button>
|
||||
<button class="btn plain" @click="prev">上一步</button>
|
||||
<button class="btn primary" @click="save">保存</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import FormTemplate from '@/components/form-template/index.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||||
|
||||
const BASE_KEY = 'patient-create-base';
|
||||
const INNER_KEY = 'patient-create-inner';
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
customerSource: '',
|
||||
firstVisitDate: '',
|
||||
diseaseTag: '',
|
||||
remark: ''
|
||||
});
|
||||
const form = reactive({});
|
||||
const baseForm = reactive({});
|
||||
const forms = computed(() => ({ ...baseForm, ...form }));
|
||||
|
||||
const items = [
|
||||
{ title: 'customerSource', name: '患者来源', type: 'select', operateType: 'formCell', required: false, range: ['线上咨询', '同事推荐', '客户推荐', '其他'] },
|
||||
{ title: 'firstVisitDate', name: '首次就诊日期', type: 'date', operateType: 'formCell', required: false },
|
||||
{ title: 'diseaseTag', name: '慢病标签', type: 'select', operateType: 'formCell', required: false, range: ['糖尿病', '高血压', '冠心病', '慢阻肺'] },
|
||||
{ title: 'remark', name: '备注', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 }
|
||||
];
|
||||
const items = ref([]);
|
||||
|
||||
const rules = {};
|
||||
const filterRule = {
|
||||
reference(formModel) {
|
||||
const customerSource = Array.isArray(formModel.customerSource)
|
||||
? formModel.customerSource
|
||||
: typeof formModel.customerSource === 'string'
|
||||
? [formModel.customerSource]
|
||||
: [];
|
||||
return ['同事推荐', '客户推荐'].includes(customerSource[0]) && customerSource.length === 1;
|
||||
},
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
function normalizeChangeValue(value) {
|
||||
if (value && typeof value === 'object' && 'value' in value) return value.value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
if (!options.length) return [];
|
||||
if (typeof options[0] === 'string') return options.filter((i) => typeof i === 'string');
|
||||
if (typeof options[0] === 'object') {
|
||||
return options
|
||||
.map((i) => {
|
||||
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||
return { label: String(label || value), value: String(value) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeTemplateItem(item) {
|
||||
const next = { ...(item || {}) };
|
||||
|
||||
if (next.operateType === 'custom') next.operateType = 'formCell';
|
||||
|
||||
const originalType = next.type;
|
||||
const customTypeMap = {
|
||||
customerSource: 'select',
|
||||
customerStage: 'select',
|
||||
tag: 'multiSelectAndOther',
|
||||
reference: 'input',
|
||||
selectWwuser: 'select',
|
||||
files: 'files',
|
||||
corpProject: 'select',
|
||||
diagnosis: 'textarea',
|
||||
BMI: 'input',
|
||||
bloodPressure: 'textarea',
|
||||
selfMultipleDiseases: 'textarea',
|
||||
};
|
||||
if (originalType && customTypeMap[originalType]) {
|
||||
next.__originType = originalType;
|
||||
next.type = customTypeMap[originalType];
|
||||
}
|
||||
|
||||
const aliasTypeMap = {
|
||||
text: 'input',
|
||||
string: 'input',
|
||||
number: 'input',
|
||||
integer: 'input',
|
||||
int: 'input',
|
||||
};
|
||||
if (next.type && aliasTypeMap[next.type]) {
|
||||
next.type = aliasTypeMap[next.type];
|
||||
if (!next.inputType && (originalType === 'number' || originalType === 'integer' || originalType === 'int')) next.inputType = 'number';
|
||||
}
|
||||
|
||||
const rawRange = next.range || next.options || next.optionList || next.values || [];
|
||||
const range = normalizeOptions(rawRange);
|
||||
|
||||
if (next.type === 'select' || next.type === 'selectAndOther' || next.type === 'selectAndImage') {
|
||||
next.range = range;
|
||||
} else if (next.type === 'radio') {
|
||||
if (range.length && typeof range[0] === 'object') {
|
||||
next.type = 'select';
|
||||
next.range = range;
|
||||
} else {
|
||||
next.range = Array.isArray(rawRange) ? rawRange : [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!next.operateType) next.operateType = 'formCell';
|
||||
next.required = Boolean(next.required);
|
||||
|
||||
if (next.type === 'input' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 20;
|
||||
if (next.type === 'textarea' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 200;
|
||||
return next;
|
||||
}
|
||||
|
||||
async function loadInternalTemplate() {
|
||||
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||
if (!corpId) return;
|
||||
|
||||
loading('加载中...');
|
||||
const res = await api('getCurrentTemplate', { corpId, templateType: 'internalTemplate' });
|
||||
hideLoading();
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取模板失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const temp = res?.data && typeof res.data === 'object' ? res.data : res;
|
||||
const list = Array.isArray(temp.templateList) ? temp.templateList : [];
|
||||
items.value = list
|
||||
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||
.filter((i) => i.operateType !== 'onlyRead')
|
||||
.map(normalizeTemplateItem);
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
const base = uni.getStorageSync(BASE_KEY);
|
||||
if (!base || typeof base !== 'object') {
|
||||
uni.showToast({ title: '请先填写基础信息', icon: 'none' });
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
Object.assign(baseForm, base);
|
||||
|
||||
const cached = uni.getStorageSync(INNER_KEY);
|
||||
if (cached && typeof cached === 'object') {
|
||||
Object.assign(form, cached);
|
||||
}
|
||||
|
||||
if (!doctorInfo.value && account.value?.openid) {
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
await loadInternalTemplate();
|
||||
});
|
||||
|
||||
function onChange({ title, value }) {
|
||||
form[title] = value;
|
||||
form[title] = normalizeChangeValue(value);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
function prev() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function save() {
|
||||
function getUserId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||
}
|
||||
|
||||
function normalizeFormValue(value) {
|
||||
if (value && typeof value === 'object' && 'value' in value) return value.value;
|
||||
if (Array.isArray(value)) return value.map(normalizeFormValue);
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeFormObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') return {};
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
acc[key] = normalizeFormValue(obj[key]);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function buildPayload(base, inner) {
|
||||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
const corpId = String(account.value?.corpId || team?.corpId || base?.corpId || '') || '';
|
||||
const baseForm = normalizeFormObject(base);
|
||||
const innerForm = normalizeFormObject(inner);
|
||||
const payload = {
|
||||
...baseForm,
|
||||
...innerForm,
|
||||
corpId,
|
||||
teamId: baseForm.teamId || (team?.teamId ? String(team.teamId) : ''),
|
||||
creator: getUserId(),
|
||||
addMethod: 'manual',
|
||||
};
|
||||
|
||||
// 兼容旧字段命名(历史缓存)
|
||||
if (payload.gender && !payload.sex) payload.sex = payload.gender;
|
||||
if (payload.idNo && !payload.idCard) payload.idCard = payload.idNo;
|
||||
if (payload.idType && !payload.cardType) payload.cardType = payload.idType;
|
||||
if (Array.isArray(payload.teamId)) payload.teamId = payload.teamId[0] || '';
|
||||
|
||||
if (payload.customerSource && typeof payload.customerSource === 'string') {
|
||||
payload.customerSource = [payload.customerSource];
|
||||
}
|
||||
|
||||
return { payload, team };
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (formRef.value?.verify && !formRef.value.verify()) return;
|
||||
|
||||
const base = uni.getStorageSync(BASE_KEY) || {};
|
||||
const payload = { ...base, ...form };
|
||||
|
||||
uni.setStorageSync(INNER_KEY, { ...form });
|
||||
|
||||
// 这里后续对接真实新增接口:payload 即最终提交数据
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '已完成新增(mock)。后续将对接真实建档接口。',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
const { payload, team } = buildPayload(base, form);
|
||||
if (!payload.corpId || !payload.teamId || !payload.creator) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
loading('请求中...');
|
||||
try {
|
||||
const createTeamId = Array.isArray(payload.teamId) ? (payload.teamId[0] || '') : String(payload.teamId || '');
|
||||
const res = await api('addCustomer', {
|
||||
createTeamId,
|
||||
createTeamName: team?.name || '',
|
||||
params: payload,
|
||||
});
|
||||
hideLoading();
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '新增失败');
|
||||
return;
|
||||
}
|
||||
|
||||
toast('新增成功');
|
||||
uni.removeStorageSync(BASE_KEY);
|
||||
uni.removeStorageSync(INNER_KEY);
|
||||
// 返回到病例列表
|
||||
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
||||
uni.navigateBack({ delta: 2 });
|
||||
} catch (e) {
|
||||
hideLoading();
|
||||
toast('新增失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -90,16 +287,20 @@ function save() {
|
||||
.page {
|
||||
height: 100vh;
|
||||
background: #f6f6f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -116,15 +317,21 @@ function save() {
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
height: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
|
||||
@ -19,12 +19,16 @@
|
||||
|
||||
<!-- Search Results -->
|
||||
<scroll-view v-if="searchQuery" scroll-y class="search-results">
|
||||
<view v-if="searchResults.length === 0" class="empty-state">
|
||||
<view v-if="searching && searchResults.length === 0" class="empty-state">
|
||||
<text class="empty-text">搜索中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="searchResults.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无搜索结果</text>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view v-for="(patient, index) in searchResults" :key="index" class="patient-card">
|
||||
<view v-for="(patient, index) in searchResults" :key="index" class="patient-card" @click="goDetail(patient)">
|
||||
<!-- Row 1 -->
|
||||
<view class="card-row-top">
|
||||
<view class="patient-info">
|
||||
@ -58,97 +62,135 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import useDebounce from '@/utils/useDebounce';
|
||||
import { toast } from '@/utils/widget';
|
||||
|
||||
// State
|
||||
const searchQuery = ref('');
|
||||
const searchResultsRaw = ref([]);
|
||||
const searching = ref(false);
|
||||
|
||||
// Mock all patients data (same as case.vue)
|
||||
const allPatients = [
|
||||
{
|
||||
letter: 'A',
|
||||
data: [
|
||||
{
|
||||
name: '安乐', gender: '男', age: 45, tags: ['糖尿病'],
|
||||
record: { type: '门诊', date: '2026.1.10', diagnosis: '2型糖尿病' },
|
||||
createTime: '2026.1.19 14:30', creator: '李医生', phone: '13888888888', hospitalId: '1001'
|
||||
},
|
||||
{
|
||||
name: '奥利奥', gender: '女', age: 22, tags: [], record: null,
|
||||
createTime: '2026.1.15 09:00', creator: '王医生', phone: '13999999999', hospitalId: '1002'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
letter: 'L',
|
||||
data: [
|
||||
{
|
||||
name: '李珊珊', gender: '女', age: 37, tags: ['糖尿病', '高血压'],
|
||||
record: { type: '门诊', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
|
||||
createTime: '2026.1.10 10:20', creator: '张医生', phone: '13666666666', hospitalId: '1003'
|
||||
},
|
||||
{
|
||||
name: '李珊珊', gender: '女', age: 37, tags: [],
|
||||
record: { type: '住院', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
|
||||
createTime: '2025.12.30 11:00', creator: '张医生', phone: '13666666666', hospitalId: '1003'
|
||||
},
|
||||
{
|
||||
name: '李某某', gender: '女', age: 37, tags: [], record: null,
|
||||
createTime: '2025.12.01 08:30', creator: '系统导入', phone: '13555555555', hospitalId: '1004'
|
||||
},
|
||||
{
|
||||
name: '李四', gender: '男', age: 50, tags: ['高血压'], record: null,
|
||||
createTime: '2026.1.18 16:45', creator: '管理员', phone: '13444444444', hospitalId: '1005'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
letter: 'Z',
|
||||
data: [
|
||||
{
|
||||
name: '张三', gender: '男', age: 28, tags: [], record: null,
|
||||
createTime: '2026.1.19 10:00', creator: '赵医生', phone: '13333333333', hospitalId: '1006'
|
||||
},
|
||||
{
|
||||
name: '张敏', gender: '女', age: 32, tags: ['高血压'],
|
||||
record: { type: '门诊', date: '2025.12.15', diagnosis: '高血压' },
|
||||
createTime: '2025.11.20 15:15', creator: '孙医生', phone: '13222222222', hospitalId: '1007'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
|
||||
function asArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function getUserId() {
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||
}
|
||||
|
||||
function getTeamContext() {
|
||||
const cached = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||
const corpId = String(cached.corpId || account.value?.corpId || '') || '';
|
||||
const teamId = String(cached.teamId || '') || '';
|
||||
return { corpId, teamId };
|
||||
}
|
||||
|
||||
function formatPatient(raw) {
|
||||
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string');
|
||||
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string');
|
||||
|
||||
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
|
||||
const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean);
|
||||
const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || '');
|
||||
|
||||
return {
|
||||
...raw,
|
||||
_id: raw?._id || raw?.id || '',
|
||||
name: raw?.name || raw?.customerName || '',
|
||||
gender: raw?.sex || raw?.gender || '',
|
||||
age: raw?.age ?? '',
|
||||
tags: rawTags.length ? rawTags : (rawTagNames.length ? rawTagNames : tagIds),
|
||||
mobiles,
|
||||
mobile,
|
||||
record: null,
|
||||
createTime: raw?.createTime || '',
|
||||
creator: raw?.creatorName || raw?.creator || '',
|
||||
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
|
||||
};
|
||||
}
|
||||
|
||||
// Computed
|
||||
const searchResults = computed(() => {
|
||||
if (!searchQuery.value) return [];
|
||||
|
||||
const q = searchQuery.value.toLowerCase();
|
||||
let results = [];
|
||||
|
||||
allPatients.forEach(group => {
|
||||
group.data.forEach(p => {
|
||||
if (p.name.includes(q) ||
|
||||
(p.phone && p.phone.includes(q)) ||
|
||||
(p.hospitalId && p.hospitalId.includes(q))) {
|
||||
results.push(p);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
return searchResultsRaw.value || [];
|
||||
});
|
||||
|
||||
// Methods
|
||||
const handleSearch = () => {
|
||||
// Search logic handled by computed property
|
||||
};
|
||||
const doSearch = useDebounce(async () => {
|
||||
const q = String(searchQuery.value || '').trim();
|
||||
if (!q) {
|
||||
searchResultsRaw.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getUserId();
|
||||
const { corpId, teamId } = getTeamContext();
|
||||
if (!corpId || !teamId || !userId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
searching.value = true;
|
||||
const res = await api('searchCorpCustomerWithFollowTime', {
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
name: q,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
searching.value = false;
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '搜索失败');
|
||||
return;
|
||||
}
|
||||
const payload =
|
||||
res && typeof res === 'object'
|
||||
? res.data && typeof res.data === 'object' && !Array.isArray(res.data)
|
||||
? res.data
|
||||
: res
|
||||
: {};
|
||||
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : [];
|
||||
searchResultsRaw.value = list.map(formatPatient);
|
||||
}, 600);
|
||||
|
||||
const handleSearch = () => doSearch();
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
searchResultsRaw.value = [];
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const goDetail = (patient) => {
|
||||
const id = patient?._id || patient?.id || patient?.mobile || patient?.phone || '';
|
||||
uni.setStorageSync(DETAIL_STORAGE_KEY, {
|
||||
_id: id,
|
||||
name: patient.name,
|
||||
sex: patient.gender,
|
||||
age: patient.age,
|
||||
mobile: patient.mobile,
|
||||
mobiles: Array.isArray(patient.mobiles) ? patient.mobiles : (patient.mobile ? [patient.mobile] : []),
|
||||
createTime: patient.createTime,
|
||||
creator: patient.creator,
|
||||
});
|
||||
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(String(id))}` });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -5,7 +5,8 @@ const urlsConfig = {
|
||||
getCorpMemberHomepageInfo: 'getCorpMemberHomepageInfo',
|
||||
getTeamBaseInfo: 'getTeamBaseInfo',
|
||||
getTeamData: 'getTeamData',
|
||||
queryWxJoinedTeams: 'queryWxJoinedTeams',
|
||||
getTeamBymember: 'getTeamBymember',
|
||||
getCurrentTemplate: 'getCurrentTemplate',
|
||||
wxAppLogin: 'wxAppLogin',
|
||||
getDeptList: 'getRealDeptList',
|
||||
getHospitalList: 'getRealHospital',
|
||||
@ -19,11 +20,14 @@ const urlsConfig = {
|
||||
},
|
||||
member: {
|
||||
addCustomer: 'add',
|
||||
updateCustomer: 'update',
|
||||
bindMiniAppArchive: "bindMiniAppArchive",
|
||||
getCustomerByCustomerId: 'getCustomerByCustomerId',
|
||||
getMiniAppCustomers: 'getMiniAppCustomers',
|
||||
getTeamCustomers: 'getTeamCustomers',
|
||||
getUnbindMiniAppCustomers: 'getUnbindMiniAppCustomers',
|
||||
searchCorpCustomer: 'searchCorpCustomer',
|
||||
searchCorpCustomerWithFollowTime: 'searchCorpCustomerWithFollowTime',
|
||||
unbindMiniAppArchive: 'unbindMiniAppArchive',
|
||||
},
|
||||
wecom: {
|
||||
@ -65,4 +69,3 @@ export default async function api(urlId, data) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user