Files
threeonecheck_web/components/hazard/HazardFormPopup.vue

1183 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 主表单弹窗 -->
<u-popup v-model:show="popupVisible" mode="center" round="20" :safe-area-inset-bottom="false" @close="handleClose">
<view class="popup-content">
<view class="popup-header">
<view class="popup-title text-bold">{{ title }}</view>
<view class="popup-close" @click="handleClose">×</view>
</view>
<scroll-view class="popup-body" scroll-y :style="bodyStyle">
<view
v-if="showDraftBanner"
class="draft-banner"
>
<view class="draft-banner-left">
<text class="cuIcon-info draft-banner-icon"></text>
<text>已自动恢复您上次未提交的内容</text>
</view>
<text class="draft-banner-action" @click="emit('clear-draft')">清空草稿</text>
</view>
<view class="flex margin-bottom">
<view class="text-gray">检查形式</view>
<view class="text-red">*</view>
</view>
<scroll-view class="hazard-choose-scroll" scroll-x :show-scrollbar="false">
<view class="hazard-choose-row">
<view
v-for="(item, index) in SOURCE_OPTIONS"
:key="item.id"
class="hazard-choose-tag hazard-choose-tag--source"
:class="{ 'hazard-choose-tag--active': formData.source === index }"
@tap.stop="onChooseSelect('source', index)"
>
<text class="hazard-choose-tag__text">{{ item.title }}</text>
</view>
</view>
</scroll-view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患图片</view>
<view class="text-red">*</view>
</view>
<up-upload
:fileList="fileList"
@afterRead="afterRead"
@delete="deletePic"
name="1"
multiple
imageMode="aspectFill"
:maxCount="10"
></up-upload>
<canvas
:canvas-id="canvasId"
:width="canvasWidth"
:height="canvasHeight"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px',
position: 'fixed',
left: '-9999px',
top: '-9999px'
}"
></canvas>
<view class="text-gray text-sm">必填请上传现场照片作为隐患证据</view>
<view class="ai-btn-wrapper margin-top">
<button class="ai-analyze-btn" :loading="aiAnalyzing" :disabled="aiAnalyzing" @click="handleAiAnalyze">
<text v-if="!aiAnalyzing" class="cuIcon-magic ai-btn-icon"></text>
{{ aiAnalyzing ? 'AI识别中...' : 'AI 识别隐患' }}
</button>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患标题</view>
<view class="text-red">*</view>
</view>
<up-input placeholder="请输入内容" border="surround" v-model="formData.title"></up-input>
<view class="text-sm text-gray margin-top-xs">请用简洁的语言概括隐患要点</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患等级</view>
<view class="text-red">*</view>
</view>
<scroll-view class="hazard-choose-scroll" scroll-x :show-scrollbar="false">
<view class="hazard-choose-row">
<view
v-for="(item, index) in LEVEL_OPTIONS"
:key="item.id"
class="hazard-choose-tag hazard-choose-tag--level"
:class="{ 'hazard-choose-tag--active': formData.level === index }"
@tap.stop="onChooseSelect('level', index)"
>
<text class="hazard-choose-tag__text">{{ item.title }}</text>
</view>
</view>
</scroll-view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患位置</view>
<view class="text-red">*</view>
</view>
<view class="address-box">
<up-input class="address-input-wrapper" v-model="address" placeholder="请输入地址" border="surround"></up-input>
<button class="btn-address bg-blue" @tap.stop="chooseLocation">选择地址</button>
</view>
<view class="text-gray text-sm margin-top-xs">办公楼3层东侧消防通道生产车间A区设备旁等或点击"选择地址"按钮在地图上选择</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">法律依据</view>
</view>
<view class="select-trigger" @click="openLawPopup">
<view class="select-value" :class="{ placeholder: !formData.regulationName }">
{{ formData.regulationName || '请选择法律依据' }}
</view>
<text class="cuIcon-unfold"></text>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患区域</view>
</view>
<view class="select-trigger" @click="openAreaPicker">
<view class="select-value" :class="{ placeholder: !areaName }">
{{ areaName || '请选择隐患区域' }}
</view>
<text class="cuIcon-unfold"></text>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患描述</view>
<view class="text-red">*</view>
</view>
<up-textarea v-model="formData.description" placeholder="请输入内容"></up-textarea>
<view class="text-gray text-sm margin-top-xs">请详细说明隐患现状潜在风险及影响范围</view>
<view class="text-gray margin-bottom margin-top">隐患标签</view>
<view class="hazard-choose-wrap">
<view
v-for="(item, index) in tagOptions"
:key="item.id"
class="hazard-choose-tag hazard-choose-tag--tag"
:class="{ 'hazard-choose-tag--active': formData.tagIndex === index }"
@tap.stop="onChooseSelect('tagIndex', index)"
>
<text class="hazard-choose-tag__text">{{ item.title }}</text>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="handleClose">取消</button>
<button class="btn-confirm bg-blue" :loading="submitting" @click="handleConfirm">确定</button>
</view>
</view>
</u-popup>
<!-- 选择法规弹出框 -->
<u-popup :show="showLawPopup" mode="center" round="20" :safe-area-inset-bottom="false" @close="showLawPopup = false">
<view class="law-popup">
<view class="popup-header">
<view class="popup-title">选择法律依据</view>
<view class="popup-close" @click="showLawPopup = false">×</view>
</view>
<view class="search-box">
<text class="cuIcon-search search-icon"></text>
<input class="search-input" v-model="lawKeyword" placeholder="请输入关键词搜索" @confirm="searchRegulation" />
<text class="search-btn" @click="searchRegulation">搜索</text>
</view>
<scroll-view class="law-list" scroll-y @scrolltolower="loadMoreLaw">
<view v-if="lawLoading && lawList.length === 0" class="loading-tip">加载中...</view>
<view v-else-if="!lawLoading && lawList.length === 0" class="empty-tip">暂无数据</view>
<template v-else>
<view
class="law-item"
:class="{ 'law-item-active': selectedLawId === item.id }"
v-for="item in lawList"
:key="item.id"
@click="selectLaw(item)"
>
<view class="law-title">{{ item.depict }}</view>
<view class="law-basis text-gray">{{ item.legalBasis }}</view>
</view>
<view v-if="lawLoading" class="loading-tip">加载中...</view>
</template>
</scroll-view>
<view class="law-popup-footer">
<button class="btn-cancel" @click="showLawPopup = false">取消</button>
<button class="btn-confirm bg-blue" @click="confirmLaw">确定</button>
</view>
</view>
</u-popup>
<!-- 区域选择弹窗 -->
<u-popup :show="showAreaPicker" mode="bottom" round="20" @close="showAreaPicker = false">
<view class="picker-popup">
<view class="picker-header">
<view class="picker-cancel" @click="showAreaPicker = false">取消</view>
<view class="picker-title">选择隐患区域</view>
<view class="picker-confirm" @click="confirmAreaSelect">确定</view>
</view>
<scroll-view class="picker-body" scroll-y>
<view
v-for="item in areaList"
:key="item.id"
class="picker-item"
:class="{ 'picker-item-active': tempAreaId === item.id }"
@click="tempAreaId = item.id"
>
<view class="flex align-center">
<view class="area-color-dot" :style="{ backgroundColor: item.color }"></view>
<text>{{ item.name }}</text>
</view>
<text v-if="tempAreaId === item.id" class="cuIcon-check text-blue"></text>
</view>
<view v-if="areaList.length === 0" class="text-gray text-center padding">暂无区域数据</view>
</scroll-view>
</view>
</u-popup>
</template>
<script setup>
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue';
import { addHiddenDanger, getHiddenDangerLabelList, getRegulationList, analyzeHazardImage } from '@/request/api.js';
import { getAreaList } from '@/request/three_one_api/area.js';
import { createUploadListHandlers } from '@/utils/upload.js';
import {
LEVEL_OPTIONS,
SOURCE_OPTIONS,
AI_LEVEL_MAP,
createEmptyHazardPayload,
buildHiddenDangerParams
} from './hazardForm.js';
const props = defineProps({
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: '填写隐患信息'
},
mode: {
type: String,
default: 'collect'
},
modelValue: {
type: Object,
default: null
},
extraParams: {
type: Object,
default: () => ({})
},
showDraftBanner: {
type: Boolean,
default: false
},
bodyHeight: {
type: String,
default: ''
},
canvasId: {
type: String,
default: 'hazardFormWatermarkCanvas'
}
});
const emit = defineEmits(['update:show', 'update:modelValue', 'success', 'confirm', 'clear-draft']);
const popupVisible = computed({
get: () => props.show,
set: (value) => emit('update:show', value)
});
const isApiSuccess = (res) => Number(res?.code) === 0 || Number(res?.code) === 200;
const bodyStyle = computed(() => (props.bodyHeight ? { height: props.bodyHeight } : {}));
const canvasWidth = ref(300);
const canvasHeight = ref(300);
const submitting = ref(false);
const aiAnalyzing = ref(false);
const syncingFromParent = ref(false);
const formData = reactive({
title: '',
level: 0,
source: 0,
description: '',
tagIndex: 0,
tagId: null,
regulationId: null,
regulationName: ''
});
const address = ref('');
const lng = ref(0);
const lat = ref(0);
const areaId = ref('');
const areaName = ref('');
const fileList = ref([]);
const tagOptions = ref([]);
const showLawPopup = ref(false);
const lawKeyword = ref('');
const selectedLawId = ref(null);
const selectedLawName = ref('');
const lawList = ref([]);
const lawLoading = ref(false);
const lawPageNum = ref(1);
const lawPageSize = ref(10);
const hasMoreLaw = ref(true);
const showAreaPicker = ref(false);
const areaList = ref([]);
const tempAreaId = ref('');
const choosingLocation = ref(false);
const suppressModelEmit = ref(false);
const LOCATION_PICKER_DRAFT_KEY = 'hazard_form_location_picker_draft';
const uploadInstance = getCurrentInstance();
const { afterRead, deletePic } = createUploadListHandlers(fileList, {
watermark: {
canvasId: props.canvasId,
canvasWidthRef: canvasWidth,
canvasHeightRef: canvasHeight,
instance: uploadInstance
}
});
function getPayload() {
syncTagIdFromIndex();
return {
formData: {
title: formData.title,
level: formData.level,
source: formData.source,
description: formData.description,
tagIndex: formData.tagIndex,
tagId: formData.tagId,
regulationId: formData.regulationId,
regulationName: formData.regulationName
},
address: address.value,
lng: lng.value,
lat: lat.value,
areaId: areaId.value,
areaName: areaName.value,
fileList: fileList.value,
tagOptions: tagOptions.value
};
}
function applyPayload(payload) {
if (!payload) return;
syncingFromParent.value = true;
const data = payload.formData || {};
formData.title = data.title || '';
formData.level = data.level ?? 0;
formData.source = data.source ?? 0;
formData.description = data.description || '';
formData.tagIndex = data.tagIndex ?? 0;
formData.tagId = data.tagId ?? null;
formData.regulationId = data.regulationId ?? null;
formData.regulationName = data.regulationName || '';
address.value = payload.address || '';
lng.value = payload.lng || 0;
lat.value = payload.lat || 0;
areaId.value = payload.areaId || '';
areaName.value = payload.areaName || '';
fileList.value = payload.fileList ? [...payload.fileList] : [];
selectedLawId.value = data.regulationId ?? null;
selectedLawName.value = data.regulationName || '';
syncingFromParent.value = false;
}
function hasPayloadContent(payload) {
if (!payload) return false;
const data = payload.formData || {};
const files = payload.fileList || [];
return !!(
data.title ||
data.description ||
data.regulationName ||
payload.address ||
(files.length > 0)
);
}
function getLocationPickerDraftKey() {
return props.extraParams?.locationDraftKey || LOCATION_PICKER_DRAFT_KEY;
}
function saveLocationPickerDraft() {
const snapshot = getPayload();
try {
uni.setStorageSync(getLocationPickerDraftKey(), snapshot);
} catch (error) {
console.error('保存选点草稿失败:', error);
}
return snapshot;
}
function loadLocationPickerDraft() {
try {
const draft = uni.getStorageSync(getLocationPickerDraftKey());
return draft && typeof draft === 'object' ? draft : null;
} catch (error) {
return null;
}
}
function clearLocationPickerDraft() {
try {
uni.removeStorageSync(getLocationPickerDraftKey());
} catch (error) {
console.error('清除选点草稿失败:', error);
}
}
function restoreAfterLocationPicker(patch = {}) {
const draft = loadLocationPickerDraft();
if (!draft) return false;
applyPayload({
...draft,
...patch,
formData: {
...(draft.formData || {}),
...(patch.formData || {})
},
fileList: patch.fileList ?? draft.fileList ?? []
});
clearLocationPickerDraft();
return true;
}
function reopenPopupAfterLocation() {
nextTick(() => {
choosingLocation.value = false;
popupVisible.value = true;
emitModelValue();
});
}
function resetForm() {
applyPayload(createEmptyHazardPayload());
selectedLawId.value = null;
selectedLawName.value = '';
tempAreaId.value = '';
clearLocationPickerDraft();
}
function syncTagIdFromIndex() {
const selectedTag = tagOptions.value[formData.tagIndex];
formData.tagId = selectedTag?.id ?? formData.tagId ?? null;
}
function onChooseSelect(field, index) {
formData[field] = Number(index);
if (field === 'tagIndex') {
syncTagIdFromIndex();
}
emitModelValue();
}
function emitModelValue() {
if (suppressModelEmit.value || syncingFromParent.value || props.modelValue === null) return;
// collect / direct 均不在编辑过程中回写父组件,避免 v-model 与内部状态互相触发递归更新
if (props.mode === 'collect' || props.mode === 'direct') return;
emit('update:modelValue', getPayload());
}
function closePopup() {
popupVisible.value = false;
}
function handleClose() {
closePopup();
}
function validateForm() {
if (fileList.value.length === 0) {
uni.showToast({ title: '请上传隐患图片', icon: 'none' });
return false;
}
if (!formData.title) {
uni.showToast({ title: '请输入隐患标题', icon: 'none' });
return false;
}
if (!address.value) {
uni.showToast({ title: '请输入隐患位置', icon: 'none' });
return false;
}
if (!formData.description) {
uni.showToast({ title: '请输入隐患描述', icon: 'none' });
return false;
}
return true;
}
async function handleConfirm() {
if (!validateForm()) return;
const payload = getPayload();
if (props.mode === 'collect') {
// 先回写数据再关弹窗,避免父组件在 show=false 时提前清空索引导致 confirm 丢失
if (props.modelValue !== null) {
emit('update:modelValue', payload);
}
emit('confirm', payload);
closePopup();
uni.showToast({ title: '隐患信息已暂存', icon: 'success' });
return;
}
submitting.value = true;
try {
const params = buildHiddenDangerParams(payload, props.extraParams);
const res = await addHiddenDanger(params);
if (isApiSuccess(res)) {
suppressModelEmit.value = true;
closePopup();
emit('success', { ...payload, hazardId: res.data });
await nextTick();
resetForm();
uni.showToast({ title: res.msg || '新增成功', icon: 'success' });
} else {
uni.showToast({ title: res.msg || '新增失败', icon: 'none' });
}
} catch (error) {
console.error(error);
uni.showToast({ title: '请求失败', icon: 'none' });
} finally {
suppressModelEmit.value = false;
submitting.value = false;
}
}
function openLawPopup() {
showLawPopup.value = true;
if (lawList.value.length === 0) {
fetchRegulationList();
}
}
async function fetchRegulationList(isLoadMore = false) {
if (lawLoading.value) return;
lawLoading.value = true;
try {
const params = {
pageNum: lawPageNum.value,
pageSize: lawPageSize.value,
status: 1
};
if (lawKeyword.value?.trim()) {
params.keyword = lawKeyword.value.trim();
}
const res = await getRegulationList(params);
if (res.code === 0) {
const records = res.data.records || res.data || [];
lawList.value = isLoadMore ? [...lawList.value, ...records] : records;
const total = res.data.total || 0;
hasMoreLaw.value = lawList.value.length < total;
}
} catch (error) {
console.error('获取法规列表失败:', error);
} finally {
lawLoading.value = false;
}
}
function searchRegulation() {
lawPageNum.value = 1;
lawList.value = [];
hasMoreLaw.value = true;
fetchRegulationList();
}
function loadMoreLaw() {
if (!hasMoreLaw.value || lawLoading.value) return;
lawPageNum.value++;
fetchRegulationList(true);
}
function selectLaw(item) {
selectedLawId.value = item.id;
selectedLawName.value = item.depict || item.keyword || '';
}
function confirmLaw() {
if (selectedLawId.value) {
formData.regulationId = selectedLawId.value;
formData.regulationName = selectedLawName.value;
}
showLawPopup.value = false;
emitModelValue();
}
function openAreaPicker() {
tempAreaId.value = areaId.value;
showAreaPicker.value = true;
}
async function fetchAreaList() {
try {
const res = await getAreaList();
if (res.code === 0 && res.data?.records) {
areaList.value = res.data.records;
}
} catch (error) {
console.error('获取区域列表失败:', error);
}
}
function confirmAreaSelect() {
if (tempAreaId.value) {
areaId.value = tempAreaId.value;
const selected = areaList.value.find((item) => item.id === tempAreaId.value);
areaName.value = selected ? selected.name : '';
}
showAreaPicker.value = false;
emitModelValue();
}
function chooseLocation() {
choosingLocation.value = true;
saveLocationPickerDraft();
closePopup();
setTimeout(() => {
uni.getLocation({
type: 'gcj02',
success: (locationRes) => {
openChooseLocation(locationRes.latitude, locationRes.longitude);
},
fail: () => {
openChooseLocation();
}
});
}, 300);
}
function openChooseLocation(latitude, longitude) {
const options = {};
if (latitude != null && longitude != null) {
options.latitude = latitude;
options.longitude = longitude;
}
uni.chooseLocation({
...options,
success: (res) => {
restoreAfterLocationPicker({
address: res.address + (res.name ? `(${res.name})` : ''),
lng: res.longitude,
lat: res.latitude
});
reopenPopupAfterLocation();
},
fail: (err) => {
restoreAfterLocationPicker();
reopenPopupAfterLocation();
if (err.errMsg && err.errMsg.indexOf('cancel') === -1) {
uni.showToast({ title: '选择位置失败', icon: 'none' });
}
}
});
}
async function handleAiAnalyze() {
const imageFiles = fileList.value.filter((f) => {
return f.status === 'success' && f.url.toLowerCase().match(/\.(jpg|jpeg|png|gif|bmp|webp)$/);
});
if (imageFiles.length === 0) {
uni.showToast({ title: '请先上传隐患图片', icon: 'none' });
return;
}
aiAnalyzing.value = true;
try {
const analyzeRes = await analyzeHazardImage({ imageUrl: imageFiles[0].url });
if (analyzeRes.code === 0 && analyzeRes.data) {
const aiData = analyzeRes.data;
if (aiData.title) formData.title = aiData.title;
if (aiData.description) formData.description = aiData.description;
if (aiData.level) {
const levelIndex = AI_LEVEL_MAP[aiData.level];
if (levelIndex !== undefined) {
formData.level = levelIndex;
}
}
emitModelValue();
uni.showToast({ title: 'AI分析完成已自动填充', icon: 'success', duration: 2000 });
} else {
uni.showToast({ title: analyzeRes.msg || 'AI分析失败', icon: 'none' });
}
} catch (error) {
console.error('AI分析接口调用失败:', error);
uni.showToast({ title: 'AI分析失败请重试', icon: 'none' });
} finally {
aiAnalyzing.value = false;
}
}
async function fetchTagOptions() {
try {
const res = await getHiddenDangerLabelList();
if (res.code === 0) {
tagOptions.value = res.data.map((item) => ({
id: item.id,
title: item.name
}));
syncTagIdFromIndex();
}
} catch (error) {
console.error('获取标签列表失败:', error);
}
}
watch(
() => props.modelValue,
(val) => {
// direct 模式由组件内部自持表单,不与父组件 v-model 双向同步
if (props.mode === 'direct') return;
if (hasPayloadContent(val)) {
applyPayload(val);
}
},
{ deep: true, immediate: true }
);
watch(
() => props.show,
(val) => {
if (val && areaList.value.length === 0) {
fetchAreaList();
}
if (!val) return;
// 地图选点返回时由选点回调恢复,避免空 modelValue 覆盖已填内容
if (choosingLocation.value) {
return;
}
const locationDraft = loadLocationPickerDraft();
if (locationDraft) {
applyPayload(locationDraft);
clearLocationPickerDraft();
return;
}
if (hasPayloadContent(props.modelValue)) {
applyPayload(props.modelValue);
}
}
);
fetchTagOptions();
fetchAreaList();
defineExpose({
getPayload,
applyPayload,
resetForm,
validateForm
});
</script>
<style lang="scss" scoped>
.popup-content {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
color: #333;
}
.popup-close {
font-size: 40rpx;
color: #999;
line-height: 1;
}
}
.popup-body {
padding: 30rpx;
max-height: 900rpx;
overflow-y: auto;
}
.draft-banner {
font-size: 24rpx;
background-color: #fff7eb;
border: 1rpx solid #ffe4cc;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 16rpx 20rpx;
border-radius: 8rpx;
color: #fa8c16;
}
.draft-banner-left {
display: flex;
flex-direction: row;
align-items: center;
}
.draft-banner-icon {
margin-right: 10rpx;
}
.draft-banner-action {
cursor: pointer;
padding: 0 10rpx;
color: #2667e9;
font-weight: bold;
}
.hazard-choose-scroll {
width: 100%;
white-space: nowrap;
}
.hazard-choose-row {
display: inline-flex;
flex-direction: row;
align-items: center;
padding-right: 20rpx;
}
.hazard-choose-wrap {
display: flex;
flex-wrap: wrap;
}
.hazard-choose-tag {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
background: #fff;
flex-shrink: 0;
margin-right: 12rpx;
&--source {
width: 152rpx;
height: 64rpx;
}
&--level {
width: 183rpx;
height: 72rpx;
}
&--tag {
min-width: 120rpx;
height: 64rpx;
padding: 0 24rpx;
margin-bottom: 12rpx;
}
&--active {
background: #2667e9;
border-color: #2667e9;
.hazard-choose-tag__text {
color: #fff;
}
}
&__text {
font-size: 24rpx;
line-height: 1.2;
color: #606266;
text-align: center;
}
}
.ai-btn-wrapper {
display: flex;
justify-content: flex-end;
}
.ai-analyze-btn {
display: flex;
align-items: center;
justify-content: center;
height: 72rpx;
padding: 0 32rpx;
font-size: 28rpx;
color: #fff;
background: linear-gradient(135deg, #4facfe 0%, #2668ea 100%);
border-radius: 36rpx;
border: none;
&::after {
border: none;
}
.ai-btn-icon {
margin-right: 8rpx;
font-size: 30rpx;
}
&[disabled] {
opacity: 0.7;
}
}
.popup-footer {
display: flex;
border-top: 1rpx solid #eee;
button {
flex: 1;
height: 90rpx;
line-height: 90rpx;
border-radius: 0;
font-size: 30rpx;
&::after {
border: none;
}
}
.btn-cancel {
background: #fff;
color: #666;
}
.btn-confirm {
color: #fff;
}
}
.address-box {
display: flex;
align-items: center;
gap: 20rpx;
.address-input-wrapper {
flex: 1;
}
.btn-address {
flex-shrink: 0;
height: 70rpx;
line-height: 70rpx;
padding: 0 30rpx;
font-size: 26rpx;
border-radius: 8rpx;
color: #fff;
&::after {
border: none;
}
}
}
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 20rpx 24rpx;
.select-value {
flex: 1;
font-size: 28rpx;
color: #333;
&.placeholder {
color: #c0c4cc;
}
}
}
.picker-popup {
background: #fff;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.picker-cancel {
font-size: 28rpx;
color: #999;
}
.picker-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.picker-confirm {
font-size: 28rpx;
color: #2667e9;
}
}
.picker-body {
max-height: 600rpx;
padding: 0 30rpx;
}
.picker-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 30rpx;
color: #333;
&:last-child {
border-bottom: none;
}
&.picker-item-active {
color: #2667e9;
}
}
}
.area-color-dot {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.law-popup {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
max-height: 80vh;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.popup-close {
font-size: 40rpx;
color: #999;
}
}
}
.search-box {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 40rpx;
padding: 16rpx 24rpx;
margin: 20rpx 30rpx;
.search-icon {
font-size: 28rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
background: transparent;
border: none;
}
.search-btn {
color: #2667e9;
font-size: 26rpx;
margin-left: 16rpx;
}
}
.loading-tip,
.empty-tip {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 26rpx;
}
.law-list {
max-height: 400rpx;
padding: 0 30rpx;
}
.law-item {
padding: 24rpx;
border: 2rpx solid #e5e5e5;
border-radius: 12rpx;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #333;
.law-title {
line-height: 1.5;
margin-bottom: 8rpx;
font-weight: bold;
}
.law-basis {
font-size: 24rpx;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.law-item-active {
border-color: #2667e9;
background: #f0f6ff;
}
.law-popup-footer {
display: flex;
padding: 30rpx;
border-top: 1rpx solid #eee;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
border-radius: 40rpx;
&::after {
border: none;
}
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-confirm {
background: #2667e9;
color: #fff;
}
}
</style>