Files
qncV4uni-app/src/pages/toolbox/query.vue

611 lines
14 KiB
Vue
Raw Normal View History

2026-05-16 15:47:07 +08:00
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getToolboxItem } from '@/config/toolboxRegistry'
import { postToolboxQuery } from '@/api/toolbox'
definePage({
style: {
navigationBarTitleText: '工具查询',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
enablePullDownRefresh: false,
},
})
const toolKey = ref('')
const tool = ref<ReturnType<typeof getToolboxItem>>(null)
const form = ref<Record<string, string>>({})
const loading = ref(false)
const result = ref<Record<string, any> | null>(null)
const error = ref('')
// 选择框相关状态
const popup = ref<any>(null)
const currentField = ref<any>(null)
const pickerValue = ref([0])
const indicatorStyle = `'height: 50px'`
onLoad((query) => {
const key = (query?.key as string) || ''
toolKey.value = key
tool.value = getToolboxItem(key)
if (!tool.value) {
error.value = '未找到该工具'
}
else {
uni.setNavigationBarTitle({ title: tool.value.name })
// 初始化表单默认值
if (tool.value.fields) {
tool.value.fields.forEach(field => {
if (field.default !== undefined && !form.value[field.key]) {
form.value[field.key] = field.default
}
})
}
// 如果工具标记了自动查询或不需要输入参数,打开即查
if (tool.value.autoQuery || tool.value.fields.length === 0) {
handleQuery()
}
}
})
// 获取选择框当前选中项的索引
function getSelectIndex(field, value) {
if (!field.options || !Array.isArray(field.options)) {
return 0
}
const defaultValue = field.options[0]?.value || ''
const targetValue = value || defaultValue
return field.options.findIndex(opt => opt.value === targetValue)
}
// 获取选择框当前选中项的标签
function getSelectedLabel(field, value) {
if (!field.options || !Array.isArray(field.options) || !value) {
return null
}
const option = field.options.find(opt => opt.value === value)
return option ? option.label : null
}
// 显示自定义选择器
function showPicker(field) {
currentField.value = field
const currentValue = form.value[field.key] || field.options[0]?.value || ''
const index = field.options.findIndex(opt => opt.value === currentValue)
pickerValue.value = [index]
popup.value.open()
}
// 关闭选择器
function closePicker() {
popup.value.close()
}
// 确认选择
function confirmPicker() {
if (currentField.value) {
const index = pickerValue.value[0]
form.value[currentField.value.key] = currentField.value.options[index].value
closePicker()
}
}
// 选择器变化处理
function onPickerChange(e) {
pickerValue.value = e.detail.value
}
async function handleQuery() {
if (!tool.value)
return
error.value = ''
result.value = null
revealedKeys.value = new Set()
if (!tool.value.validate(form.value)) {
error.value = tool.value.validateMsg
return
}
loading.value = true
try {
const res = await postToolboxQuery(toolKey.value, form.value)
if (res.code === 200 && res.data?.result) {
result.value = res.data.result
}
else {
error.value = res.msg || '查询失败'
}
}
catch {
error.value = '网络错误,请稍后重试'
}
finally {
loading.value = false
}
}
const resultEntries = computed(() => {
if (!result.value || !tool.value?.resultLabels)
return []
return Object.entries(tool.value.resultLabels)
.filter(([key]) => result.value![key] !== undefined)
.map(([key, labelOrObj]) => {
const val = result.value![key]
const display = val === '' || val === null ? '无' : val
if (typeof labelOrObj === 'object' && labelOrObj !== null) {
return { key, label: labelOrObj.label, hidden: !!labelOrObj.hidden, value: display }
}
return { key, label: labelOrObj, hidden: false, value: display }
})
})
const revealedKeys = ref<Set<string>>(new Set())
function toggleReveal(key: string) {
if (revealedKeys.value.has(key)) {
revealedKeys.value.delete(key)
}
else {
revealedKeys.value.add(key)
}
}
const resultList = computed(() => {
if (!result.value || tool.value?.resultType !== 'list')
return []
const list = result.value.list
if (!Array.isArray(list))
return []
return list
})
const listLabelEntries = computed(() => {
if (!tool.value?.resultLabels)
return []
return Object.entries(tool.value.resultLabels)
})
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view v-if="tool" class="card">
<view class="card-desc">
<text class="desc-text">{{ tool.desc }}</text>
</view>
<!-- 动态表单 -->
<view class="form-area">
<view v-for="field in tool.fields" :key="field.key" class="field">
<text class="field-label">{{ field.label }}</text>
<!-- 选择框 -->
<picker
v-if="field.type === 'select'"
mode="selector"
:range="field.options.map(opt => opt.label)"
:value="getSelectIndex(field, form[field.key])"
@change="(e: any) => {
const index = e.detail.value
form[field.key] = field.options[index].value
}"
>
<view class="field-input field-picker">
{{ getSelectedLabel(field, form[field.key]) || field.placeholder }}
</view>
</picker>
<!-- 日期选择器 -->
<picker
v-else-if="field.type === 'date'"
mode="date"
:value="form[field.key] || ''"
@change="(e: any) => (form[field.key] = e.detail.value)"
>
<view class="field-input field-picker">
{{ form[field.key] || field.placeholder }}
</view>
</picker>
<!-- 文本域 -->
<textarea
v-else-if="field.type === 'textarea'"
class="field-input field-textarea"
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:value="form[field.key] || ''"
placeholder-class="field-ph"
@input="(e: any) => (form[field.key] = e.detail.value)"
/>
<!-- 普通输入框 -->
<input
v-else
class="field-input"
:type="field.type"
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:value="form[field.key] || ''"
placeholder-class="field-ph"
confirm-type="done"
@input="(e: any) => (form[field.key] = e.detail.value)"
>
</view>
</view>
<!-- 查询按钮 -->
<button
class="query-btn"
:disabled="loading"
@tap="handleQuery"
>
{{ loading ? '查询中...' : '立即查询' }}
</button>
<!-- 错误提示 -->
<view v-if="error" class="msg-area msg-error">
<text class="msg-text">{{ error }}</text>
</view>
<!-- 结果展示 - 普通键值对 -->
<view v-if="resultEntries.length > 0 && tool?.resultType !== 'list'" class="result-area">
<view class="result-title">查询结果</view>
<view class="result-list">
<view
v-for="item in resultEntries"
:key="item.key"
class="result-row"
>
<text class="result-label">{{ item.label }}</text>
<!-- 隐藏字段点击显示/隐藏 -->
<view v-if="item.hidden" class="result-reveal-wrap">
<view
v-if="!revealedKeys.has(item.key)"
class="result-reveal-btn"
@tap="toggleReveal(item.key)"
>
点击查看
</view>
<template v-else>
<text class="result-value">{{ item.value }}</text>
<text class="result-hide-btn" @tap="toggleReveal(item.key)">收起</text>
</template>
</view>
<!-- 普通字段 -->
<text v-else class="result-value">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 结果展示 - 列表型 -->
<view v-if="resultList.length > 0" class="result-area">
<view class="result-title">查询结果</view>
<view class="result-card-list">
<view
v-for="(item, idx) in resultList"
:key="idx"
class="result-card-item"
>
<view
v-for="([fieldKey, fieldLabel], fIdx) in listLabelEntries"
:key="fieldKey"
class="card-item-row"
>
<template v-if="item[fieldKey] !== undefined && item[fieldKey] !== ''">
<text v-if="fIdx === 0" class="card-item-index">{{ idx + 1 }}</text>
<text v-else class="card-item-index-placeholder" />
<text class="card-item-field-label">{{ typeof fieldLabel === 'object' ? fieldLabel.label : fieldLabel }}</text>
<text class="card-item-field-value">{{ item[fieldKey] }}</text>
</template>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty">
<text class="empty-text">未找到该工具</text>
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
.page-root {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%);
}
.scrollarea {
flex: 1;
min-height: 0;
height: 0;
}
.page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.card {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
padding: 32rpx 28rpx;
border: 1rpx solid #e5e6f0;
box-shadow:
0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.card-desc {
margin-bottom: 28rpx;
}
.desc-text {
font-size: 24rpx;
color: #86909c;
line-height: 1.5;
}
.form-area {
margin-bottom: 28rpx;
}
.field {
margin-bottom: 20rpx;
}
.field-label {
display: block;
font-size: 24rpx;
color: #4e5969;
margin-bottom: 8rpx;
}
.field-input {
width: 100%;
height: 80rpx;
background: #f7f8fa;
border: 1rpx solid #e5e6f0;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1d2129;
box-sizing: border-box;
}
.field-picker {
display: flex;
align-items: center;
color: #1d2129;
}
.field-picker:empty::before {
content: attr(placeholder);
color: #c9cdd4;
}
.field-textarea {
height: 200rpx;
padding: 16rpx 24rpx;
line-height: 1.6;
}
.field-ph {
color: #c9cdd4;
}
.query-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #1768ff, #4e8cff);
color: #ffffff;
font-size: 30rpx;
font-weight: 500;
border-radius: 16rpx;
border: none;
margin-bottom: 20rpx;
}
.query-btn[disabled] {
opacity: 0.6;
}
.msg-area {
padding: 16rpx 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.msg-error {
background: #fff2f0;
border: 1rpx solid #ffccc7;
}
.msg-text {
font-size: 24rpx;
color: #f53f3f;
}
.result-area {
margin-top: 8rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f2f3f5;
}
.result-title {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 16rpx;
}
.result-list {
background: #f7f8fa;
border-radius: 16rpx;
padding: 8rpx 0;
}
.result-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
}
.result-label {
font-size: 26rpx;
color: #86909c;
}
.result-value {
font-size: 26rpx;
color: #1d2129;
font-weight: 500;
flex: 1;
text-align: right;
}
.result-reveal-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12rpx;
}
.result-reveal-btn {
font-size: 24rpx;
color: #ffffff;
background: linear-gradient(135deg, #1768ff, #4e8cff);
padding: 6rpx 24rpx;
border-radius: 24rpx;
}
.result-hide-btn {
font-size: 22rpx;
color: #86909c;
flex-shrink: 0;
}
.empty {
display: flex;
justify-content: center;
align-items: center;
padding: 120rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #86909c;
}
/* 列表型结果卡片 */
.result-card-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.result-card-item {
background: #ffffff;
border: 1rpx solid #e5e6f0;
border-radius: 16rpx;
padding: 20rpx 24rpx;
}
.card-item-row {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 4rpx 0;
}
.card-item-index {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
background: linear-gradient(135deg, #1768ff, #4e8cff);
color: #ffffff;
font-size: 22rpx;
font-weight: 600;
border-radius: 50%;
}
.card-item-index-placeholder {
flex-shrink: 0;
width: 40rpx;
}
.card-item-field-label {
flex-shrink: 0;
font-size: 24rpx;
color: #86909c;
min-width: 80rpx;
}
.card-item-field-value {
flex: 1;
font-size: 26rpx;
color: #1d2129;
line-height: 1.6;
font-weight: 500;
}
/* 自定义选择器样式 */
.picker-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20rpx;
height: 88rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.picker-header text {
font-size: 32rpx;
}
.picker-header text:first-child {
color: #606266;
}
.picker-header text:last-child {
color: #1768ff;
}
.picker-view {
width: 100%;
height: 400rpx;
}
.picker-item {
line-height: 100rpx;
text-align: center;
font-size: 28rpx;
color: #333;
}
</style>