feat: 地区选择器改走接口,修复辅料列表 key 重复

静态地区树改为 regionPicker API;新建就诊人异步解析地址;处方辅料列表 :key 改用 index 避免 drug_id 重复。
This commit is contained in:
李琦
2026-10-09 10:02:59 +08:00
parent 1dff190e14
commit e6e51ac805
8 changed files with 486 additions and 662 deletions

72
api/regionPicker.js Normal file
View File

@@ -0,0 +1,72 @@
/**
* 省市区选择器接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
* 按 loginMode 切角色前缀,对齐 upload.js:医生走 region-picker,业务员/平台/诊所管理员走带前缀路由
* 为什么:doctor.php 里同名路由只会命中最先注册的医生鉴权组,业务员 token 会被 401
*/
import { req } from '@/common/js/index.js'
/**
* 按当前登录角色解析 region-picker 前缀与鉴权头标记
* @returns {{ prefix: string, header: Record<string, string> }}
*/
function resolveRegionPickerRoute() {
const mode = uni.getStorageSync('loginMode')
if (mode === 'salesperson') {
return {
prefix: '/newApi/salesperson-region-picker/',
header: { _salesperson: '1' },
}
}
if (mode === 'platform_admin') {
return {
prefix: '/newApi/platform-admin-region-picker/',
header: { _platformAdmin: '1' },
}
}
if (mode === 'clinic_admin') {
return {
prefix: '/newApi/clinic-admin-region-picker/',
header: { _clinicAdmin: '1' },
}
}
// 医生 / 药师等默认
return {
prefix: '/newApi/region-picker/',
header: {},
}
}
function regionPickerRequest(path, data) {
const { prefix, header } = resolveRegionPickerRoute()
return req.request({
url: `${prefix}${path}`,
method: 'GET',
data,
header,
})
}
/** 按 pid 拉一层子节点 */
export function getRegionChildren(pid = 0) {
return regionPickerRequest('children', { pid })
}
/** 地名/首拼/全拼合一搜索 */
export function searchRegions(keyword, limit = 30) {
return regionPickerRequest('search', { keyword, limit })
}
/** 按省+叶回显 */
export function resolveRegionPair(provinceId, cityId) {
return regionPickerRequest('resolve', {
province_id: provinceId,
city_id: cityId,
})
}
/** 批量 ID 解析 */
export function resolveRegionIds(ids) {
return regionPickerRequest('resolve', {
ids: (ids || []).join(','),
})
}

View File

@@ -1,632 +1,293 @@
import { addressOption } from '@/subPackages/sub_clinic_salesperson/utils/address.js';
/**
* 省市区选择器状态与组装工具:直连 region-picker API
* 对外仍输出 [provinceId, leafId],兼容门店/建就诊人 address 字段
* 为什么:本地 addressOption 二级树已废弃,三级树 + 拼音搜索走数据库
*/
import {
getRegionChildren,
resolveRegionPair,
searchRegions,
} from '@/api/regionPicker.js'
import { unwrapXkApi } from '@/utils/api-response.js'
const displayCache = new Map()
const pairCache = new Map()
/** 跨级搜索结果 */
/** 列表面板统一行 */
const MUNICIPALITY_LABELS = new Set([
'北京市',
'天津市',
'上海市',
'重庆市',
]);
let provinceIndexCache = null;
function isPrefectureCityLabel(label) {
return label.endsWith('市') && !label.endsWith('自治区');
function cacheKey(provinceId, leafId) {
return `${provinceId}_${leafId}`
}
function isDistrictLikeLabel(label) {
return /(?:区|县|旗)$/.test(label);
function levelFromNumber(level) {
if (level <= 1) return 'province'
if (level === 2) return 'city'
return 'district'
}
function detectMunicipality(province, children) {
if (MUNICIPALITY_LABELS.has(province.label)) {
return true;
}
if (!province.label.endsWith('市') || children.length === 0) {
return false;
}
const districtCount = children.filter((c) => isDistrictLikeLabel(c.label)).length;
return districtCount / children.length >= 0.8;
function toNode(row) {
if (!row) return undefined
return {
value: row.id,
label: row.name,
pvalue: row.pid,
level: row.level,
hasChildren: !!row.has_children,
}
}
function buildProvinceIndexList() {
return (addressOption).map((province) => {
const children = (province)
.children ?? [];
return {
province: {
value: province.value,
label: province.label,
pvalue: province.pvalue,
},
children: children.map((c) => ({
value: c.value,
label: c.label,
pvalue: c.pvalue,
})),
isMunicipality: detectMunicipality(province, children),
};
});
/** 组装对外 address 值 [provinceId, leafId] */
export function buildAddressOutput(provinceId, cityId, districtId) {
if (!provinceId) return undefined
const leafId = districtId != null ? districtId : cityId
if (!leafId) return undefined
return [provinceId, leafId]
}
function getProvinceIndexList() {
if (!provinceIndexCache) {
provinceIndexCache = buildProvinceIndexList();
}
return provinceIndexCache;
/**
* 同步解析:只读本地缓存(由 ensureResolvedAddress 填充)
* 为什么:部分展示位是同步的,不能直接 await
*/
export function resolveAddressValue(value) {
if (!value || value.length < 2) return {}
const provinceId = value[0]
const leafId = value[1]
return (
pairCache.get(cacheKey(provinceId, leafId)) || {
provinceId,
cityId: leafId,
}
)
}
function findProvinceIndex(provinceId) {
if (!provinceId) {
return undefined;
}
return getProvinceIndexList().find((p) => p.province.value === provinceId);
/**
* 异步解析并写入缓存(含软删节点,后端 resolve 不过滤 deleted_at)
*/
export async function ensureResolvedAddress(value) {
if (!value || value.length < 2) return {}
const provinceId = value[0]
const leafId = value[1]
const key = cacheKey(provinceId, leafId)
if (pairCache.has(key)) {
return pairCache.get(key)
}
try {
const res = await resolveRegionPair(provinceId, leafId)
const { ok, payload } = unwrapXkApi(res)
if (!ok || !payload) {
return { provinceId, cityId: leafId }
}
const resolved = {
provinceId,
cityId: undefined,
districtId: undefined,
province: payload.province ? toNode(payload.province) : undefined,
city: payload.city
? {
value: payload.city.id,
label: payload.city.name,
pvalue: payload.city.pid,
level: payload.city.level,
}
: undefined,
district: payload.district
? {
value: payload.district.id,
label: payload.district.name,
pvalue: payload.district.pid,
level: payload.district.level,
}
: undefined,
pathLabel: payload.path_label || '',
}
if (payload.district && !payload.city) {
resolved.districtId = leafId
} else if (payload.district) {
resolved.cityId = payload.city && payload.city.id
resolved.districtId = payload.district.id
} else {
resolved.cityId = leafId
}
pairCache.set(key, resolved)
if (resolved.pathLabel) {
displayCache.set(key, resolved.pathLabel)
}
return resolved
} catch (e) {
return { provinceId, cityId: leafId }
}
}
function findChild(
provinceIndex,
childId,
) {
if (!childId) {
return undefined;
}
return provinceIndex.children.find((c) => c.value === childId);
}
function getCityNamePrefix(label) {
return label.replace(/市$/, '');
}
/** 省级列表 */
export function getProvinces() {
return getProvinceIndexList().map((p) => p.province);
}
/** 市级列表(直辖市返回空) */
export function getCities(provinceId) {
const index = findProvinceIndex(provinceId);
if (!index || index.isMunicipality) {
return [];
}
return index.children;
}
/** 区级列表 */
export function getDistricts(
provinceId,
cityId,
) {
const index = findProvinceIndex(provinceId);
if (!index) {
return [];
}
if (index.isMunicipality) {
return index.children;
}
if (!cityId) {
return [];
}
const city = findChild(index, cityId);
if (!city || !isPrefectureCityLabel(city.label)) {
return [];
}
const prefix = getCityNamePrefix(city.label);
return index.children.filter(
(c) =>
c.value !== cityId &&
!isPrefectureCityLabel(c.label) &&
c.label.startsWith(prefix),
);
}
/** 当前层级候选列表 */
export function getLevelOptions(
level,
ctx,
) {
if (level === 'province') {
return getProvinces();
}
if (level === 'city') {
return getCities(ctx.provinceId);
}
return getDistricts(ctx.provinceId, ctx.cityId);
}
/** 选择某级后的下一层级(无则 undefined) */
export function getNextLevel(
currentLevel,
ctx,
) {
const index = findProvinceIndex(ctx.provinceId);
if (!index) {
return undefined;
}
if (currentLevel === 'province') {
return index.isMunicipality ? 'district' : 'city';
}
if (currentLevel === 'city') {
if (!ctx.cityId) {
return undefined;
}
const districts = getDistricts(ctx.provinceId, ctx.cityId);
return districts.length > 0 ? 'district' : undefined;
}
return undefined;
}
/** 当前层级选中后是否可直接完成(叶节点) */
export function canCompleteAfterSelect(
level,
ctx,
item,
) {
const index = findProvinceIndex(ctx.provinceId);
if (!index) {
return false;
}
if (level === 'district') {
return true;
}
if (level === 'city') {
if (index.isMunicipality) {
return false;
}
if (!isPrefectureCityLabel(item.label)) {
return true;
}
return getDistricts(ctx.provinceId, item.value).length === 0;
}
return false;
}
/** 组装对外 address 值 */
export function buildAddressOutput(
provinceId,
cityId,
districtId,
) {
if (!provinceId) {
return undefined;
}
const leafId = districtId ?? cityId;
if (!leafId) {
return undefined;
}
return [provinceId, leafId];
}
/** 由 address 反查省/市/区 */
export function resolveAddressValue(
value,
) {
if (!value || value.length < 2) {
return {};
}
const [provinceId, leafId] = value;
const index = findProvinceIndex(provinceId);
if (!index) {
return { provinceId };
}
const leaf = findChild(index, leafId);
if (!leaf) {
return { provinceId, province: index.province };
}
if (index.isMunicipality) {
return {
provinceId,
districtId: leafId,
province: index.province,
district: leaf,
};
}
const isLeafDistrict =
!isPrefectureCityLabel(leaf.label) && isDistrictLikeLabel(leaf.label);
if (isPrefectureCityLabel(leaf.label)) {
return {
provinceId,
cityId: leafId,
province: index.province,
city: leaf,
};
}
if (isLeafDistrict) {
for (const city of index.children.filter((c) =>
isPrefectureCityLabel(c.label),
)) {
const prefix = getCityNamePrefix(city.label);
if (leaf.label.startsWith(prefix)) {
return {
provinceId,
cityId: city.value,
districtId: leafId,
province: index.province,
city,
district: leaf,
};
}
}
return {
provinceId,
cityId: leafId,
province: index.province,
city: leaf,
};
}
return {
provinceId,
cityId: leafId,
province: index.province,
city: leaf,
};
}
/** 面包屑展示项 */
export function formatBreadcrumbItems(
resolved,
) {
const items = [];
if (resolved.provinceId && resolved.province) {
items.push({
level: 'province',
id: resolved.provinceId,
label: resolved.province.label,
});
}
const index = findProvinceIndex(resolved.provinceId);
if (index?.isMunicipality) {
if (resolved.districtId && resolved.district) {
items.push({
level: 'district',
id: resolved.districtId,
label: resolved.district.label,
});
}
return items;
}
if (resolved.cityId && resolved.city) {
items.push({
level: 'city',
id: resolved.cityId,
label: resolved.city.label,
});
}
if (resolved.districtId && resolved.district) {
items.push({
level: 'district',
id: resolved.districtId,
label: resolved.district.label,
});
}
return items;
}
/** 触发器展示文案 */
/** 同步展示:优先缓存;未命中触发异步并先返回空串 */
export function formatAddressDisplay(value) {
const items = formatBreadcrumbItems(resolveAddressValue(value));
return items.map((i) => i.label).join(' / ');
if (!value || value.length < 2) return ''
const key = cacheKey(value[0], value[1])
if (displayCache.has(key)) {
return displayCache.get(key)
}
ensureResolvedAddress(value)
return ''
}
/** 当前层级搜索占位 */
export function getLevelSearchPlaceholder(level) {
const map = {
province: '搜索省',
city: '搜索市/县',
district: '搜索区',
};
return map[level];
export function formatBreadcrumbItems(resolved) {
const items = []
if (resolved.province) {
items.push({
level: 'province',
id: resolved.province.value,
label: resolved.province.label,
})
}
if (resolved.city) {
items.push({
level: 'city',
id: resolved.city.value,
label: resolved.city.label,
})
}
if (resolved.district) {
items.push({
level: 'district',
id: resolved.district.value,
label: resolved.district.label,
})
}
return items
}
/** 模糊筛选 */
export function filterAddressOptions(
options,
keyword,
) {
const q = keyword.trim().toLowerCase();
if (!q) {
return options;
}
return options.filter((o) => o.label.toLowerCase().includes(q));
export function buildBreadcrumbFromContext(ctx, labels) {
const items = []
const lab = labels || {}
if (ctx.provinceId) {
items.push({
level: 'province',
id: ctx.provinceId,
label: lab.province || String(ctx.provinceId),
})
}
if (ctx.cityId) {
items.push({
level: 'city',
id: ctx.cityId,
label: lab.city || String(ctx.cityId),
})
}
if (ctx.districtId) {
items.push({
level: 'district',
id: ctx.districtId,
label: lab.district || String(ctx.districtId),
})
}
return items
}
/** 从已选上下文生成面包屑(面板内) */
export function buildBreadcrumbFromContext(
ctx,
) {
return formatBreadcrumbItems({
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: ctx.districtId,
province: findProvinceIndex(ctx.provinceId)?.province,
city: findChild(findProvinceIndex(ctx.provinceId), ctx.cityId),
district: findChild(findProvinceIndex(ctx.provinceId), ctx.districtId),
});
/** 拉取某一层选项 */
export async function fetchLevelOptions(level, ctx) {
let pid = 0
if (level === 'city') pid = ctx.provinceId || 0
else if (level === 'district') pid = ctx.cityId || 0
const res = await getRegionChildren(pid)
const { ok, payload } = unwrapXkApi(res)
const rows = ok && Array.isArray(payload) ? payload : []
return rows.map((r) => toNode(r))
}
/** 根据面板内选中生成 ResolvedAddress */
export function contextToResolved(ctx) {
const index = findProvinceIndex(ctx.provinceId);
return {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: ctx.districtId,
province: index?.province,
city: index ? findChild(index, ctx.cityId) : undefined,
district: index ? findChild(index, ctx.districtId) : undefined,
};
export function levelOptionsToRows(level, options) {
return (options || []).map((node) => ({
level,
node,
label: node.label,
complete: !node.hasChildren,
}))
}
/** 是否直辖市 */
export function isMunicipalityProvince(provinceId) {
return !!findProvinceIndex(provinceId)?.isMunicipality;
export async function searchAddressHits(keyword) {
const kw = (keyword || '').trim()
if (!kw) return []
const res = await searchRegions(kw, 30)
const { ok, payload } = unwrapXkApi(res)
const hits = ok && Array.isArray(payload) ? payload : []
return hits.map((h) => ({
level: levelFromNumber(h.level),
node: toNode(h),
pathLabel: h.path_label || h.name,
complete: !!h.complete,
path: h.path,
}))
}
/** 打开面板时的初始层级(有回填则进入待选级,否则省) */
export function getInitialPickerLevel(
resolved,
) {
const index = findProvinceIndex(resolved.provinceId);
if (!resolved.provinceId || !index) {
return 'province';
}
if (index.isMunicipality) {
return 'district';
}
if (!resolved.cityId) {
return 'city';
}
const districts = getDistricts(resolved.provinceId, resolved.cityId);
if (!resolved.districtId && districts.length > 0) {
return 'district';
}
return 'city';
}
/** 当前级候选转列表行 */
export function levelOptionsToRows(
level,
ctx,
) {
return getLevelOptions(level, ctx).map((node) => ({
level,
node,
label: node.label,
complete: canCompleteAfterSelect(
level,
level === 'city'
? { provinceId: ctx.provinceId, cityId: node.value }
: level === 'district'
? {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: node.value,
}
: ctx,
node,
),
}));
}
/** 跨级模糊搜索(含第三级区/县) */
export function searchAddressHits(
keyword,
ctx,
) {
const q = keyword.trim().toLowerCase();
if (!q) {
return [];
}
const hits = [];
const seen = new Set();
const push = (hit) => {
const key = `${hit.level}-${hit.node.value}`;
if (!seen.has(key)) {
seen.add(key);
hits.push(hit);
}
};
if (!ctx.provinceId) {
for (const p of getProvinces()) {
if (p.label.toLowerCase().includes(q)) {
push({
level: 'province',
node: p,
pathLabel: p.label,
complete: false,
});
}
}
return hits;
}
const index = findProvinceIndex(ctx.provinceId);
if (!index) {
return hits;
}
if (index.isMunicipality) {
for (const d of index.children) {
if (d.label.toLowerCase().includes(q)) {
push({
level: 'district',
node: d,
pathLabel: `${index.province.label} / ${d.label}`,
complete: true,
});
}
}
return hits;
}
const cityNode = ctx.cityId ? findChild(index, ctx.cityId) : undefined;
if (!ctx.cityId) {
for (const child of index.children) {
if (!child.label.toLowerCase().includes(q)) {
continue;
}
const rowCtx = { provinceId: ctx.provinceId, cityId: child.value };
push({
level: 'city',
node: child,
pathLabel: `${index.province.label} / ${child.label}`,
complete: canCompleteAfterSelect('city', rowCtx, child),
});
}
for (const child of index.children.filter((c) => isPrefectureCityLabel(c.label))) {
for (const d of getDistricts(ctx.provinceId, child.value)) {
if (!d.label.toLowerCase().includes(q)) {
continue;
}
push({
level: 'district',
node: d,
pathLabel: `${index.province.label} / ${child.label} / ${d.label}`,
complete: true,
});
}
}
return hits;
}
if (cityNode && cityNode.label.toLowerCase().includes(q)) {
push({
level: 'city',
node: cityNode,
pathLabel: `${index.province.label} / ${cityNode.label}`,
complete: canCompleteAfterSelect('city', ctx, cityNode),
});
}
for (const d of getDistricts(ctx.provinceId, ctx.cityId)) {
if (d.label.toLowerCase().includes(q)) {
push({
level: 'district',
node: d,
pathLabel: `${index.province.label} / ${cityNode?.label ?? ''} / ${d.label}`,
complete: true,
});
}
}
return hits;
}
/** 搜索命中转列表行 */
export function searchHitsToRows(hits) {
return hits.map((hit) => ({
level: hit.level,
node: hit.node,
label: hit.pathLabel,
complete: hit.complete,
}));
return (hits || []).map((hit) => ({
level: hit.level,
node: hit.node,
label: hit.pathLabel,
complete: hit.complete,
}))
}
/** 应用跨级搜索选中 */
export function applySearchHit(
hit,
ctx,
) {
if (hit.level === 'province') {
return {
provinceId: hit.node.value,
cityId: undefined,
districtId: undefined,
};
}
if (hit.level === 'city') {
return {
provinceId: ctx.provinceId,
cityId: hit.node.value,
districtId: undefined,
};
}
if (hit.level === 'district' && findProvinceIndex(ctx.provinceId)?.isMunicipality) {
return {
provinceId: ctx.provinceId,
cityId: undefined,
districtId: hit.node.value,
};
}
return {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: hit.node.value,
};
export function applySearchHit(hit) {
const next = {}
const path = hit.path || []
for (let i = 0; i < path.length; i++) {
const node = path[i]
if (node.level === 1) next.provinceId = node.id
else if (node.level === 2) next.cityId = node.id
else if (node.level === 3) next.districtId = node.id
}
if (hit.level === 'province') {
next.provinceId = hit.node.value
} else if (hit.level === 'city') {
next.cityId = hit.node.value
if (!next.provinceId) next.provinceId = hit.node.pvalue
} else {
next.districtId = hit.node.value
if (!next.cityId) next.cityId = hit.node.pvalue
}
return next
}
/** 高亮项是否有下一级 */
export function rowHasNextLevel(
row,
ctx,
) {
if (row.level === 'province') {
return !!getNextLevel('province', {
provinceId: row.node.value,
cityId: undefined,
districtId: undefined,
});
}
if (row.level === 'city') {
return !!getNextLevel('city', {
provinceId: ctx.provinceId,
cityId: row.node.value,
districtId: undefined,
});
}
return false;
export function getNextLevel(level, _ctx, hasChildren) {
if (hasChildren === false) return undefined
if (level === 'province') return 'city'
if (level === 'city') return 'district'
return undefined
}
/** 点击面包屑项后截断选中并返回新上下文 */
export function truncateContextToBreadcrumb(
ctx,
item,
) {
if (item.level === 'province') {
return {
provinceId: item.id,
cityId: undefined,
districtId: undefined,
};
}
if (item.level === 'city') {
return {
provinceId: ctx.provinceId,
cityId: item.id,
districtId: undefined,
};
}
return {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: item.id,
};
export function getInitialPickerLevel(resolved) {
if (resolved.districtId) return 'district'
if (resolved.cityId) return 'city'
return 'province'
}
export function canCompleteAfterSelect(level, _ctx, item) {
if (level === 'district') return true
return !item.hasChildren
}
export function rowHasNextLevel(row) {
return !row.complete && !!(row.node && row.node.hasChildren)
}
export function truncateContextToBreadcrumb(ctx, item) {
if (item.level === 'province') {
return { provinceId: item.id }
}
if (item.level === 'city') {
return { provinceId: ctx.provinceId, cityId: item.id }
}
return {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: item.id,
}
}
export function getLevelSearchPlaceholder(level) {
if (level === 'province') return '搜索省(地名/首拼/全拼)'
if (level === 'city') return '搜索市(地名/首拼/全拼)'
return '搜索区县(地名/首拼/全拼)'
}
/** 兼容旧调用:直辖市由 hasChildren 驱动,保留空实现 */
export function isMunicipalityProvince() {
return false
}

File diff suppressed because one or more lines are too long

View File

@@ -132,7 +132,7 @@
<view
class="_name"
v-for="(ex, exIdx) in resolveExcipients(item)"
:key="ex.drug_id || exIdx"
:key="exIdx"
>
<view class="text">
<text>{{ ex.display_name }}</text>

View File

@@ -8,7 +8,7 @@
<drawer-page-container v-model="show" title="选择省市区">
<view class="drawer-body" :style="drawerBodyStyle">
<view class="search-box">
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable :adjust-position="false" />
<u-input v-model="keyword" placeholder="地名/首拼/全拼" clearable :adjust-position="false" />
</view>
<scroll-view scroll-x class="crumb-scroll">
<view class="crumbs">
@@ -22,6 +22,7 @@
</view>
</scroll-view>
<scroll-view scroll-y class="list-scroll" :style="listScrollStyle">
<view v-if="loading" class="empty">加载中...</view>
<view
v-for="row in listRows"
:key="row.rowKey"
@@ -31,7 +32,7 @@
<text class="row-label">{{ isSearching ? row.label : row.node.label }}</text>
<text v-if="!isSearching && row.showArrow" class="row-arrow">›</text>
</view>
<view v-if="!listRows.length" class="empty">无匹配结果</view>
<view v-if="!loading && !listRows.length" class="empty">无匹配结果</view>
</scroll-view>
</view>
</drawer-page-container>
@@ -39,21 +40,26 @@
</template>
<script>
/**
* 省市区选择器:直连 region-picker(懒加载 + 合一搜索)
* 对外仍 emit [provinceId, leafId]
*/
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
import {
formatAddressDisplay,
resolveAddressValue,
applySearchHit,
buildAddressOutput,
buildBreadcrumbFromContext,
canCompleteAfterSelect,
ensureResolvedAddress,
fetchLevelOptions,
formatAddressDisplay,
getInitialPickerLevel,
getNextLevel,
levelOptionsToRows,
rowHasNextLevel,
searchAddressHits,
searchHitsToRows,
applySearchHit,
rowHasNextLevel,
truncateContextToBreadcrumb,
getNextLevel,
canCompleteAfterSelect,
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
export default {
@@ -66,36 +72,32 @@ export default {
data() {
return {
show: false,
loading: false,
keyword: '',
currentLevel: 'province',
ctx: { provinceId: undefined, cityId: undefined, districtId: undefined },
crumbLabels: {},
listRows: [],
keyboardHeight: 0,
_searchTimer: null,
_displayTick: 0,
}
},
computed: {
// 依赖 _displayTick:异步 resolve 写缓存后强制刷新展示文案
displayText() {
void this._displayTick
return formatAddressDisplay(this.value)
},
isSearching() {
return !!(this.keyword || '').trim()
},
breadcrumbs() {
return buildBreadcrumbFromContext(this.ctx).map((item) => ({
return buildBreadcrumbFromContext(this.ctx, this.crumbLabels).map((item) => ({
...item,
rowKey: item.level + '-' + item.id,
}))
},
listRows() {
const rows = this.isSearching
? searchHitsToRows(searchAddressHits(this.keyword, this.ctx))
: levelOptionsToRows(this.currentLevel, this.ctx)
// 箭头在脚本里算好,模板禁止方法调用绑定(部分机型 WXML 会点不动/编译异常)
return rows.map((row) => ({
...row,
rowKey: row.level + '-' + row.node.value,
showArrow: !this.isSearching && rowHasNextLevel(row, this.ctx),
}))
},
drawerBodyStyle() {
if (!this.keyboardHeight) {
return {}
@@ -124,8 +126,25 @@ export default {
this.keyboardHeight = 0
this.unbindKeyboardListener()
},
keyword() {
if (!this.show) return
clearTimeout(this._searchTimer)
this._searchTimer = setTimeout(() => {
this.reloadList()
}, 250)
},
value: {
deep: true,
immediate: true,
handler(val) {
if (!this.show) {
this.prefetchDisplay(val)
}
},
},
},
beforeDestroy() {
clearTimeout(this._searchTimer)
this.unbindKeyboardListener()
},
methods: {
@@ -145,69 +164,144 @@ export default {
this._keyboardBound = false
this._keyboardHandler = null
},
syncFromValue() {
const resolved = resolveAddressValue(this.value)
async prefetchDisplay(val) {
await ensureResolvedAddress(val)
this._displayTick += 1
},
async syncFromValue() {
const resolved = await ensureResolvedAddress(this.value)
this.ctx = {
provinceId: resolved.provinceId,
cityId: resolved.cityId,
districtId: resolved.districtId,
}
this.currentLevel = getInitialPickerLevel(resolved)
this.crumbLabels = {
province: resolved.province && resolved.province.label,
city: resolved.city && resolved.city.label,
district: resolved.district && resolved.district.label,
}
if (resolved.districtId && resolved.cityId) {
this.currentLevel = 'district'
} else if (resolved.cityId && resolved.provinceId) {
this.currentLevel = resolved.district ? 'district' : 'city'
} else {
this.currentLevel = getInitialPickerLevel(resolved)
}
this.keyword = ''
this._displayTick += 1
},
openDrawer() {
this.syncFromValue()
async reloadList() {
this.loading = true
try {
if (this.isSearching) {
const hits = await searchAddressHits(this.keyword)
this.listRows = searchHitsToRows(hits).map((row) => ({
...row,
rowKey: row.level + '-' + row.node.value,
showArrow: false,
}))
} else {
const options = await fetchLevelOptions(this.currentLevel, this.ctx)
this.listRows = levelOptionsToRows(this.currentLevel, options).map((row) => ({
...row,
rowKey: row.level + '-' + row.node.value,
showArrow: rowHasNextLevel(row),
}))
}
} finally {
this.loading = false
}
},
async openDrawer() {
await this.syncFromValue()
this.show = true
await this.reloadList()
},
emitValue() {
async emitValue() {
const out = buildAddressOutput(this.ctx.provinceId, this.ctx.cityId, this.ctx.districtId)
if (out) this.$emit('change', out)
if (out) {
await ensureResolvedAddress(out)
this._displayTick += 1
this.$emit('change', out)
this.$emit('input', out)
}
},
onCrumb(item) {
async onCrumb(item) {
this.ctx = truncateContextToBreadcrumb(this.ctx, item)
this.currentLevel = item.level
if (item.level === 'province') {
this.currentLevel = 'city'
this.crumbLabels = { province: item.label }
} else if (item.level === 'city') {
this.currentLevel = 'district'
this.crumbLabels = {
province: this.crumbLabels.province,
city: item.label,
}
} else {
this.currentLevel = 'district'
}
this.keyword = ''
await this.reloadList()
},
onPickRow(row) {
async onPickRow(row) {
if (this.isSearching) {
const hit = searchAddressHits(this.keyword, this.ctx).find(
const hits = await searchAddressHits(this.keyword)
const hit = hits.find(
(h) => h.level === row.level && h.node.value === row.node.value
)
if (!hit) return
this.ctx = applySearchHit(hit, this.ctx)
this.ctx = applySearchHit(hit)
const parts = (hit.pathLabel || '').split('/').filter(Boolean)
this.crumbLabels = {
province: parts[0],
city: parts[1],
district: parts[2],
}
if (hit.complete) {
this.emitValue()
await this.emitValue()
this.show = false
return
}
const next = getNextLevel(hit.level, this.ctx)
const next = getNextLevel(hit.level, this.ctx, !hit.complete)
this.currentLevel = next || hit.level
this.keyword = ''
await this.reloadList()
return
}
const pickCtx = row.level === 'city'
? { provinceId: this.ctx.provinceId, cityId: row.node.value }
: row.level === 'district'
? { provinceId: this.ctx.provinceId, cityId: this.ctx.cityId, districtId: row.node.value }
: { provinceId: row.node.value, cityId: undefined, districtId: undefined }
if (row.level === 'province') {
this.ctx = { provinceId: row.node.value, cityId: undefined, districtId: undefined }
this.currentLevel = getNextLevel('province', this.ctx) || 'province'
} else if (row.level === 'city') {
this.ctx = { ...this.ctx, cityId: row.node.value, districtId: undefined }
if (canCompleteAfterSelect('city', pickCtx, row.node)) {
this.emitValue()
this.crumbLabels = { province: row.node.label }
if (canCompleteAfterSelect('province', this.ctx, row.node)) {
await this.emitValue()
this.show = false
return
}
this.currentLevel = getNextLevel('city', this.ctx) || 'city'
this.currentLevel = getNextLevel('province', this.ctx, row.node.hasChildren) || 'city'
} else if (row.level === 'city') {
this.ctx = { ...this.ctx, cityId: row.node.value, districtId: undefined }
this.crumbLabels = {
province: this.crumbLabels.province,
city: row.node.label,
}
if (canCompleteAfterSelect('city', this.ctx, row.node)) {
await this.emitValue()
this.show = false
return
}
this.currentLevel = getNextLevel('city', this.ctx, row.node.hasChildren) || 'city'
} else if (row.level === 'district') {
this.ctx = { ...this.ctx, districtId: row.node.value }
this.emitValue()
this.crumbLabels = {
...this.crumbLabels,
district: row.node.label,
}
await this.emitValue()
this.show = false
return
}
this.keyword = ''
await this.reloadList()
},
},
}

View File

@@ -92,8 +92,8 @@ import { createUserPatientApi } from '@/api/reception.js'
import { unwrapXkApi } from '@/utils/api-response.js'
import ClaimQrcodeModal from '@/subPackages/sub_workbench/components/claim-qrcode-modal/claim-qrcode-modal.vue'
import {
ensureResolvedAddress,
formatAddressDisplay,
resolveAddressValue,
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
// 省市区组件在业务员分包:禁止跨分包 import,走 pages.json usingComponents + componentPlaceholder
@@ -181,16 +181,16 @@ export default {
}
return []
},
/** 省市区选择回调:落本地 ids,并缓存中文标签供提交(避免提交时再 resolve 失败) */
onShipRegionChange(e) {
/** 省市区选择回调:落本地 ids,并异步 resolve 中文标签供提交 */
async onShipRegionChange(e) {
const val = this.unwrapRegionPickerValue(e)
this.shipRegion = val
const resolved = resolveAddressValue(val)
const resolved = await ensureResolvedAddress(val)
this.shipLabels = {
province: (resolved.province && resolved.province.label) || '',
city: (resolved.city && resolved.city.label) || '',
area: (resolved.district && resolved.district.label) || '',
region: formatAddressDisplay(val),
region: resolved.pathLabel || formatAddressDisplay(val),
}
},
validate() {
@@ -220,11 +220,8 @@ export default {
// 填了详细地址则必须选省市区,否则后端会静默跳过、开方订单无地址
const shipDetail = String(this.form.ship_detail_address || '').trim()
if (shipDetail) {
const resolved = resolveAddressValue(this.shipRegion)
const hasRegion = !!(this.shipLabels.province || this.shipLabels.city || this.shipLabels.area
|| (resolved.province && resolved.province.label)
|| (resolved.city && resolved.city.label)
|| (resolved.district && resolved.district.label))
|| (this.shipRegion && this.shipRegion.length >= 2))
if (!hasRegion) {
this.$toast('请选择省市区')
return false
@@ -237,7 +234,7 @@ export default {
}
return true
},
buildPayload() {
async buildPayload() {
const payload = {
...this.form,
age: Number(this.form.age),
@@ -245,7 +242,7 @@ export default {
waive_register_fee: this.form.waive_register_fee ? 1 : 0,
}
const detail = String(this.form.ship_detail_address || '').trim()
const resolved = resolveAddressValue(this.shipRegion)
const resolved = await ensureResolvedAddress(this.shipRegion)
const province = this.shipLabels.province
|| (resolved.province && resolved.province.label)
|| ''
@@ -255,7 +252,9 @@ export default {
const area = this.shipLabels.area
|| (resolved.district && resolved.district.label)
|| ''
const region = this.shipLabels.region || formatAddressDisplay(this.shipRegion)
const region = this.shipLabels.region
|| resolved.pathLabel
|| formatAddressDisplay(this.shipRegion)
if (detail && (province || city || area)) {
payload.ship_province = province
payload.ship_city = city
@@ -271,7 +270,7 @@ export default {
if (!this.validate()) return
this.submitting = true
try {
const res = await createUserPatientApi(this.buildPayload())
const res = await createUserPatientApi(await this.buildPayload())
const { ok, payload, message } = unwrapXkApi(res)
if (!ok) {
this.$toast(message || '创建失败')

View File

@@ -132,7 +132,7 @@
<view
class="_name"
v-for="(ex, exIdx) in resolveExcipients(item)"
:key="ex.drug_id || exIdx"
:key="exIdx"
>
<view class="text">
<text>{{ ex.display_name }}</text>

View File

@@ -549,7 +549,7 @@
<view
class="tcm-tag"
v-for="(item, exIndex) in excipients"
:key="item.id || exIndex"
:key="exIndex"
:data-idx="exIndex"
@click.stop="onExcipientTagClick"
@longpress.stop="onExcipientTagLongPress"