F5刷新-引起表单重复提交解决-<s:token />

本文介绍Struts2中如何通过token拦截器防止表单重复提交。利用同步令牌方式,在客户端与服务器端间验证请求的有效性,确保不会因浏览器刷新等操作导致数据误提交。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、简介

Struts2使用token拦截器来检查表单是否重复提交,采用的是同步令牌的方式。

同步令牌方式:服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较,看是否匹配。在处理完该请求后,且在答复发送给客户端之前,将会产生一个新的令牌,该令牌除传给客户端以外,也会将用户会话中保存的旧的令牌进行替换。这样如果用户回退到刚才的提交页面并再次提交的话,客户端传过来的令牌就和服务器端的令牌不一致,从而有效地防止了重复提交的发生。 


实现方法: 
1)token 
token拦截器进行拦截,如果为重复请求,就冲定向到名为invalid.token的Result。 
2)tokenSession 
tokenSession拦截器与token拦截器唯一的不同是在判断某个请求为重复请求之后,并不是立即重定向到名为invalid.token的Result,而是先阻塞这个重复请求,直到浏览器响应最初的正常请求,然后就可以跳转到处理正常请求后的Result了

二、token标签

复制代码
<package name="money" namespace="/money" extends="struts-default">
        <action name="transfer"  class="com.meetcomet.action.transferAction">
            <interceptor-ref name="defaultStack" />
             <!--使用token拦截器-->
            <interceptor-ref name="token" />
              <!--拦截到后的输出界面-->
               <result name="invalid.token">/index.jsp</result>           
            <result name="success">/welcome.jsp</result>
        </action>
</package>
复制代码

输入index.jsp

复制代码
<s:form action="transfer" namespace="/money">
    <!---token标签--->
    <s:token></s:token>
    <!---用于显示action的错误,因为设置的是拦截到后再次返回此页,所以设置了这个标签-->
    <s:actionerror/>
    <s:textfield label="Amount" name="amount"  value="100"/>
    <s:submit value="Transfer money" />
</s:form>
复制代码

<script setup> import { ref, reactive, onMounted, computed } from 'vue'; import { onShow } from '@dcloudio/uni-app'; // 响应式数据 const surveyList = ref([]); const total = ref(0); const loading = ref(false); const formData = reactive({ dcWjTitle: '', dcId: [], dcDept: '', state: null }); const bdrOptions = ref([]); const bdrLoading = ref(false); const pagination = reactive({ current: 1, size: 10, total: 0 }); // 被测评人选择器相关 const bdrSearchKeyword = ref(''); const showBdrList = ref(false); const filteredBdrOptions = ref([]); const showBdrPlaceholder = ref(true); // 状态选择器相关 const stateOptions = ref([ { label: '全部', value: null }, { label: '已提交', value: 1 }, { label: '未提交', value: 0 } ]); const showStateList = ref(false); // 计算属性 const selectedBdrLabel = computed(() => { if (formData.dcId.length === 0) return ''; const selected = bdrOptions.value.find(b => b.value === formData.dcId[0]); return selected ? selected.label : ''; }); const getStateLabel = (value) => { const state = stateOptions.value.find(s => s.value === value); return state ? state.label : '全部'; }; // 被测评人选择器方法 const handleBdrFocus = () => { showBdrList.value = true; showBdrPlaceholder.value = false; filterBdrOptions(); }; const handleBdrInput = (e) => { bdrSearchKeyword.value = e.detail.value; filterBdrOptions(); }; const handleBdrSelect = (item) => { formData.dcId = [item.value]; showBdrList.value = false; bdrSearchKeyword.value = ''; showBdrPlaceholder.value = false; }; const toggleBdrList = () => { showBdrList.value = !showBdrList.value; if (showBdrList.value) { filterBdrOptions(); } }; // 状态选择器方法 const handleStateSelect = (state) => { formData.state = state.value; showStateList.value = false; }; const toggleStateList = () => { showStateList.value = !showStateList.value; }; // 过滤被测评人选项 const filterBdrOptions = () => { if (!bdrSearchKeyword.value) { filteredBdrOptions.value = [...bdrOptions.value]; return; } const keyword = bdrSearchKeyword.value.toLowerCase(); filteredBdrOptions.value = bdrOptions.value.filter(item => item.label.toLowerCase().includes(keyword) ); }; // 获取问卷数据 const fetchSurveyData = async () => { try { loading.value = true; const token = uni.getStorageSync('token'); if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); uni.redirectTo({ url: '/pages/login/login' }); return; } const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/listTx', method: 'GET', data: { pageNum: pagination.current, pageSize: pagination.size, dcWjTitle: formData.dcWjTitle, dcId: formData.dcId.join(','), dcDept: formData.dcDept, state: formData.state }, header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 10000 // 增加超时设置 }); // 完整的响应日志 // console.log('API响应:', res); // 处理不同的响应格式 let responseData = null; // 处理 uni.request 的不同返回值格式 if (Array.isArray(res)) { // uni-app 返回数组格式 [error, response] if (res[0]) throw res[0]; // 如果有错误对象 responseData = res[1].data; } else { // 直接返回对象格式 responseData = res.data; } // 检查响应状态码 if (responseData?.code === 200) { surveyList.value = responseData.rows || []; total.value = responseData.total || 0; pagination.total = responseData.total || 0; } else { // 更详细的错误信息 const errorMsg = responseData?.msg || `API返回非200状态: ${responseData?.code || '未知'}`; throw new Error(errorMsg); } } catch (error) { console.error('请求错误详情:', error); // 更精确的错误分类 let userFriendlyMsg = '加载问卷数据失败'; if (error.errMsg?.includes('timeout')) { userFriendlyMsg = '请求超时,请检查网络连接'; } else if (error.errMsg?.includes('404')) { userFriendlyMsg = 'API地址不存在,请联系管理员'; } else if (error.errMsg?.includes('401')) { userFriendlyMsg = '登录已过期,请重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.message) { userFriendlyMsg = error.message; } uni.showToast({ title: userFriendlyMsg, icon: 'none', duration: 3000 }); } finally { loading.value = false; } }; // 清除被测评人选择的方法 const handleClearBdr = (e) => { e.stopPropagation(); // 阻止事件冒泡 formData.dcId = []; bdrSearchKeyword.value = ''; showBdrList.value = false; showBdrPlaceholder.value = true; }; // 获取被测评人列表 const fetchBdrList = async () => { try { const token = uni.getStorageSync('token'); if (!token) { console.warn('未获取到token,跳过获取被测评人列表'); return; } // 打印请求开始信息 // console.log('开始获取被测评人列表...'); const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/getBdrList', method: 'GET', header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 8000 }); // 详细记录原始响应 // console.log('被测评人API原始响应:', res); // 统一处理uni-app不同平台的响应格式 let response = null; if (Array.isArray(res)) { // 小程序格式 [error, response] if (res[0]) { throw res[0]; } response = res[1]; } else { // H5格式 { data, statusCode, ... } response = res; } // 检查HTTP状态码 if (response.statusCode !== 200) { throw new Error(`HTTP错误: ${response.statusCode}`); } // 解析响应体 const responseBody = response.data; // console.log('API响应体:', responseBody); // 处理不同的API响应结构 let data = []; if (Array.isArray(responseBody)) { // 情况1: 直接返回数组 data = responseBody; } else if (responseBody.data && Array.isArray(responseBody.data)) { // 情况2: { data: [...] } data = responseBody.data; } else if (responseBody.rows && Array.isArray(responseBody.rows)) { // 情况3: { rows: [...] } data = responseBody.rows; } else if (responseBody.result && Array.isArray(responseBody.result)) { // 情况4: { result: [...] } data = responseBody.result; } else { console.error('无法识别的API响应格式:', responseBody); throw new Error('无效的API响应格式'); } // 转换数据格式 bdrOptions.value = data.map(item => { // 调试输出每个项目 // console.log('被测评人原始项:', item); // 尝试可能的字段组合 return { label: item.dcName || item.name || item.username || item.nickname || '未知名称', value: item.dcId || item.id || item.userId || item.value || null }; }); // console.log('转换后的被测评人选项:', bdrOptions.value); // 初始化过滤后的选项 filteredBdrOptions.value = [...bdrOptions.value]; } catch (error) { console.error('获取被测评人失败:', error); // 更详细的错误处理 let errorMsg = '获取被测评人失败'; if (error.errMsg) { if (error.errMsg.includes('401')) { errorMsg = '认证失败,请重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.errMsg.includes('timeout')) { errorMsg = '请求超时,请检查网络并重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.errMsg.includes('404')) { errorMsg = 'API地址已过期,请重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.errMsg.includes('fail')) { errorMsg = `网络请求失败: ${error.errMsg},请联系管理员`; } } else if (error.message) { errorMsg = error.message; } uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 }); } finally { // 添加加载状态管理 bdrLoading.value = false; } }; // 搜索按钮处理 const handleSearch = () => { pagination.current = 1; fetchSurveyData(); }; // 重置按钮处理 const handleReset = () => { formData.dcWjTitle = ''; formData.dcId = []; formData.dcDept = ''; formData.state = null; bdrSearchKeyword.value = ''; showBdrList.value = false; showStateList.value = false; showBdrPlaceholder.value = true; handleSearch(); }; // 编辑/查看问卷 const handleView = (item) => { uni.navigateTo({ url: `/pages/workbench/mycp/operation/operation?id=${item.dcWjId}` }); }; // 分页大小改变 const handleSizeChange = (e) => { pagination.size = e.detail.value; pagination.current = 1; fetchSurveyData(); }; // 页码改变 const handlePageChange = (page) => { pagination.current = page; fetchSurveyData(); }; // 刷新数据 const refreshData = () => { pagination.current = 1; fetchSurveyData(); }; // 页面显示时加载数据 onShow(() => { fetchSurveyData(); fetchBdrList(); }); </script> <template> <view class="container"> <!-- 搜索表单区域 --> <view class="search-card"> <view class="form-group title-group"> <text class="form-label">问卷标题:</text> <input v-model="formData.dcWjTitle" placeholder="请输入问卷标题" class="form-input title-input" /> </view> <view class="form-row"> <!-- 被测评人选择器 --> <view class="form-group form-group-half"> <text class="form-label">被测评人:</text> <view class="search-select-container"> <view class="input-container" @click="showBdrList = true"> <!-- 已选项显示 --> <view v-if="formData.dcId.length > 0" class="selected-display"> {{ selectedBdrLabel }} <view class="delete-icon" @click.stop="handleClearBdr"> <text class="x-icon">×</text> </view> </view> <!-- 占位符 --> <text v-else-if="showBdrPlaceholder" class="placeholder-text"> 请选择被测评人 </text> <!-- 下拉箭头 --> <view class="dropdown-icon"> <text>▼</text> </view> </view> <!-- 下拉列表 --> <view v-if="showBdrList" class="dropdown-list"> <view class="search-box"> <input v-model="bdrSearchKeyword" placeholder="搜索被测评人" class="search-input" @input="filterBdrOptions" /> </view> <scroll-view scroll-y="true" class="dropdown-scroll"> <view v-for="(item, index) in filteredBdrOptions" :key="index" class="dropdown-item" :class="{ 'selected': formData.dcId[0] === item.value }" @click="handleBdrSelect(item)" > {{ item.label }} </view> <view v-if="filteredBdrOptions.length === 0" class="empty-option"> 无匹配结果 </view> </scroll-view> </view> <!-- 遮罩层 --> <view v-if="showBdrList" class="dropdown-mask" @click="showBdrList = false" ></view> </view> </view> <!-- 人员部门 --> <view class="form-group form-group-half"> <text class="form-label">人员部门:</text> <input v-model="formData.dcDept" placeholder="请输入部门" class="form-input" /> </view> </view> <!-- 提交状态和按钮组 --> <view class="form-row-bottom"> <!-- 提交状态 --> <view class="state-group"> <text class="form-label">提交状态:</text> <view class="search-select-container"> <view class="state-select-box" @click="toggleStateList"> <text class="selected-state"> {{ getStateLabel(formData.state) }} </text> <view class="dropdown-icon"> <text>▼</text> </view> </view> <!-- 状态下拉列表 --> <view v-if="showStateList" class="dropdown-list state-dropdown"> <view v-for="(state, index) in stateOptions" :key="index" class="dropdown-item" :class="{ 'selected': formData.state === state.value }" @click="handleStateSelect(state)" > {{ state.label }} </view> </view> <!-- 遮罩层 --> <view v-if="showStateList" class="dropdown-mask" @click="showStateList = false" ></view> </view> </view> <!-- 按钮组 --> <view class="button-group"> <button class="search-button" @click="handleSearch">搜索</button> <button class="reset-button" @click="handleReset">重置</button> </view> </view> </view> <!-- 数据显示区域 --> <view class="data-card"> <view class="card-header"> <button class="refresh-button" @click="refreshData">刷新数据</button> </view> <!-- 加载状态 --> <view v-if="loading" class="loading-container"> <view class="loading-spinner"></view> <text class="loading-text">加载中...</text> </view> <!-- 数据展示 --> <view v-else> <view v-for="(item, index) in surveyList" :key="index" class="data-card-item" > <view class="card-header-section"> <view class="card-title">{{ item.dcWjTitle }}</view> </view> <view class="card-body-section"> <view class="card-row"> <text class="card-label">被测评人:</text> <text class="card-value">{{ item.dcName }}</text> </view> <view class="card-row"> <text class="card-label">部门:</text> <text class="card-value">{{ item.dcDept }}</text> </view> <view class="card-row"> <text class="card-label">创建时间:</text> <text class="card-value">{{ item.createTime }}</text> </view> <view class="card-row"> <text class="card-label">提交时间:</text> <text class="card-value">{{ item.updateTime || '-' }}</text> </view> </view> <view class="card-footer-section"> <view class="status-container"> <view :class="[ 'status-tag', item.state === '1' ? 'status-submitted' : 'status-not-submitted' ]" > {{ item.state === '1' ? '已提交' : '未提交' }} </view> <view class="score">总分: {{ item.score || '0' }}</view> </view> <button class="view-button" @click="handleView(item)">编辑/查看</button> </view> </view> <!-- 空数据提示 --> <view v-if="surveyList.length === 0" class="empty"> <text>暂无数据</text> </view> </view> <!-- 分页控件 --> <view v-if="surveyList.length > 0" class="pagination-container"> <picker mode="selector" :range="[5, 10, 20, 50]" :value="[5, 10, 20, 50].indexOf(pagination.size)" @change="handleSizeChange" class="page-size-picker" > <view class="picker"> 每页 {{ pagination.size }} 条 </view> </picker> <view class="pagination-buttons"> <button :disabled="pagination.current === 1" @click="handlePageChange(pagination.current - 1)" class="page-button prev-button" > 上一页 </button> <text class="page-info"> 第 {{ pagination.current }} 页 / 共 {{ Math.ceil(pagination.total / pagination.size) }} 页 </text> <button :disabled="pagination.current >= Math.ceil(pagination.total / pagination.size)" @click="handlePageChange(pagination.current + 1)" class="page-button next-button" > 下一页 </button> </view> <text class="total-records">共 {{ pagination.total }} 条记录</text> </view> </view> </view> </template> 在删除选择的被测评人后,下面的下拉列还是显示的之前搜索出来的名字,我想要删除选择的名字后下拉列表和刚开始加载页面一样直接检索所有的名字,如果名字过多就显示前5个旁边其他的隐藏当往上划动的时候显示出隐藏的名字
最新发布
08-07
<script setup> import { ref, reactive, onMounted, computed } from 'vue'; import { onShow } from '@dcloudio/uni-app'; // 响应式数据 const surveyList = ref([]); const total = ref(0); const loading = ref(false); const formData = reactive({ dcWjTitle: '', dcId: [], dcDept: '', state: null }); const bdrOptions = ref([]); const bdrLoading = ref(false); const pagination = reactive({ current: 1, size: 10, total: 0 }); const showBdrPlaceholder = ref(true); const handleBdrFocus = () => { showBdrList.value = true; showBdrPlaceholder.value = false; // 输入框获得焦点时隐藏占位文字 filterBdrOptions(); }; // 选择被测评人 const handleBdrSelect = (item) => { formData.dcId = [item.value]; showBdrList.value = false; bdrSearchKeyword.value = ''; showBdrPlaceholder.value = false; // 选择后不显示占位文字 }; // 新增的搜索选择器相关变量 const bdrSearchKeyword = ref(''); const showBdrList = ref(false); const filteredBdrOptions = ref([]); // 状态选择器相关变量 const stateOptions = ref([ { label: '全部', value: null }, { label: '已提交', value: 1 }, { label: '未提交', value: 0 } ]); const showStateList = ref(false); // 计算选中的被测评人标签 const selectedBdrLabel = computed(() => { if (formData.dcId.length === 0) return ''; const selected = bdrOptions.value.find(b => b.value === formData.dcId[0]); return selected ? selected.label : ''; }); // 获取状态标签 const getStateLabel = (value) => { const state = stateOptions.value.find(s => s.value === value); return state ? state.label : '全部'; }; // 过滤选项 const filterBdrOptions = () => { if (!bdrSearchKeyword.value) { filteredBdrOptions.value = [...bdrOptions.value]; return; } const keyword = bdrSearchKeyword.value.toLowerCase(); filteredBdrOptions.value = bdrOptions.value.filter(item => item.label.toLowerCase().includes(keyword) ); }; // 选择状态 const handleStateSelect = (state) => { formData.state = state.value; showStateList.value = false; }; // 切换下拉列表显示 const toggleBdrList = () => { showBdrList.value = !showBdrList.value; if (showBdrList.value) { filterBdrOptions(); } }; // 切换状态列表显示 const toggleStateList = () => { showStateList.value = !showStateList.value; }; // 获取问卷数据 const fetchSurveyData = async () => { try { loading.value = true; // 获取 token const token = uni.getStorageSync('token'); if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); uni.redirectTo({ url: '/pages/login/login' }); return; } // 准备请求参数 const params = { pageNum: pagination.current, pageSize: pagination.size, dcWjTitle: formData.dcWjTitle, dcId: formData.dcId.join(','), dcDept: formData.dcDept, state: formData.state }; // 发送请求 const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/listTx', method: 'GET', data: params, header: { 'Authorization': `Bearer ${token}` } }); // 错误处理 if (res.statusCode !== 200) { throw new Error(`请求失败,状态码: ${res.statusCode}`); } // 处理响应数据 const response = res.data; if (response.code === 200) { surveyList.value = response.rows || []; total.value = response.total || 0; pagination.total = response.total || 0; } else { throw new Error(response.msg || 'API返回错误'); } } catch (error) { console.error('请求错误:', error); uni.showToast({ title: error.message || '加载失败', icon: 'none', duration: 3000 }); } finally { loading.value = false; } }; // 获取被测评人列表 const fetchBdrList = async () => { try { const token = uni.getStorageSync('token'); if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); return; } const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/getBdrList', method: 'GET', header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 8000 }); if (res.statusCode !== 200) { throw new Error(`HTTP错误: ${res.statusCode}`); } const response = res.data; let data = []; if (Array.isArray(response)) { data = response; } else if (response.data) { data = response.data; } else if (response.rows) { data = response.rows; } else { throw new Error('无效的API响应格式'); } if (!Array.isArray(data)) { throw new Error('响应数据格式错误'); } bdrOptions.value = data.map(item => ({ label: item.dcName || '未知名称', value: item.dcId || item.id || null })); // 初始化过滤后的选项 filteredBdrOptions.value = [...bdrOptions.value]; } catch (error) { console.error('获取被测评人失败:', error); let errorMsg = '获取被测评人失败'; if (error.message.includes('401')) { errorMsg = '认证失败,请重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.message.includes('timeout')) { errorMsg = '请求超时,请检查网络'; } else if (error.message.includes('404')) { errorMsg = 'API地址不存在'; } uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 }); } }; // 搜索按钮处理 const handleSearch = () => { pagination.current = 1; fetchSurveyData(); }; // 重置按钮处理 const handleReset = () => { formData.dcWjTitle = ''; formData.dcId = []; formData.dcDept = ''; formData.state = null; bdrSearchKeyword.value = ''; showBdrList.value = false; showStateList.value = false; handleSearch(); showBdrPlaceholder.value = true; // 重置后显示占位文字 }; // 编辑/查看问卷 const handleView = (item) => { uni.navigateTo({ url: `/pages/operation/operation?id=${item.dcWjId}` }); }; // 分页大小改变 const handleSizeChange = (e) => { pagination.size = e.detail.value; fetchSurveyData(); }; // 页码改变 const handlePageChange = (page) => { pagination.current = page; fetchSurveyData(); }; // 刷新数据 const refreshData = () => { pagination.current = 1; fetchSurveyData(); }; // 页面显示时加载数据 onShow(() => { fetchSurveyData(); fetchBdrList(); }); </script> <template> <view class="container"> <!-- 搜索表单区域 --> <view class="search-card"> <view class="form-group"> <text class="form-label">问卷标题:</text> <input v-model="formData.dcWjTitle" placeholder="请输入问卷标题" class="form-input" /> </view> <view class="form-row"> <!-- 被测评人选择器 --> <view class="form-group form-group-half"> <text class="form-label">被测评人:</text> <view class="search-select-container"> <!-- 搜索输入框 --> <input :value="bdrSearchKeyword" :placeholder="showBdrPlaceholder ? '搜索被测评人' : ''" class="search-input" @focus="handleBdrFocus" @input="handleBdrInput" /> <!-- 下拉列表 --> <view v-if="showBdrList" class="dropdown-list"> <scroll-view scroll-y="true" class="dropdown-scroll"> <view v-for="(item, index) in filteredBdrOptions" :key="index" class="dropdown-item" :class="{ 'selected': formData.dcId[0] === item.value }" @click="handleBdrSelect(item)" > {{ item.label }} </view> <view v-if="filteredBdrOptions.length === 0" class="empty-option"> 无匹配结果 </view> </scroll-view> </view> <!-- 已选项显示 --> <view v-if="formData.dcId.length > 0 && !showBdrList" class="selected-display"> {{ selectedBdrLabel }} </view> <!-- 下拉箭头 --> <view class="dropdown-icon" @click="toggleBdrList"> <text>▼</text> </view> <!-- 遮罩层 --> <view v-if="showBdrList" class="dropdown-mask" @click="showBdrList = false" ></view> </view> </view> <!-- 人员部门 --> <view class="form-group form-group-half"> <text class="form-label">人员部门:</text> <input v-model="formData.dcDept" placeholder="请输入部门" class="form-input" /> </view> </view> <view class="form-row-bottom"> <!-- 提交状态 --> <view class="form-group state-group"> <text class="form-label">提交状态:</text> <view class="search-select-container"> <!-- 状态选择框 --> <view class="state-select-box" @click="toggleStateList" > <text class="selected-state"> {{ getStateLabel(formData.state) }} </text> <view class="dropdown-icon"> <text>▼</text> </view> </view> <!-- 状态下拉列表 --> <view v-if="showStateList" class="dropdown-list state-dropdown"> <view v-for="(state, index) in stateOptions" :key="index" class="dropdown-item" :class="{ 'selected': formData.state === state.value }" @click="handleStateSelect(state)" > {{ state.label }} </view> </view> <!-- 遮罩层 --> <view v-if="showStateList" class="dropdown-mask" @click="showStateList = false" ></view> </view> </view> <!-- 按钮组 --> <view class="button-group"> <button class="search-button" @click="handleSearch">搜索</button> <button class="reset-button" @click="handleReset">重置</button> </view> </view> </view> <!-- 数据显示区域 --> <view class="data-card"> <view class="card-header"> <button class="refresh-button" @click="refreshData">刷新数据</button> </view> <!-- 加载状态 --> <view v-if="loading" class="loading-container"> <view class="loading-spinner"></view> <text class="loading-text">加载中...</text> </view> <!-- 数据展示 --> <view v-else> <view v-for="(item, index) in surveyList" :key="index" class="data-card-item"> <view class="card-header-section"> <view class="card-title">{{ item.dcWjTitle }}</view> </view> <view class="card-body-section"> <view class="card-row"> <text class="card-label">被测评人:</text> <text class="card-value">{{ item.dcName }}</text> </view> <view class="card-row"> <text class="card-label">部门:</text> <text class="card-value">{{ item.dcDept }}</text> </view> <view class="card-row"> <text class="card-label">创建时间:</text> <text class="card-value">{{ item.createTime }}</text> </view> <view class="card-row"> <text class="card-label">提交时间:</text> <text class="card-value">{{ item.updateTime || '-' }}</text> </view> </view> <view class="card-footer-section"> <view class="status-container"> <view :class="['status-tag', item.state === '1' ? 'status-submitted' : 'status-not-submitted']"> {{ item.state === '1' ? '已提交' : '未提交' }} </view> <view class="score">总分: {{ item.score || '0' }}</view> </view> <button class="view-button" @click="handleView(item)">编辑/查看</button> </view> </view> <!-- 空数据提示 --> <view v-if="surveyList.length === 0" class="empty"> <text>暂无数据</text> </view> </view> <!-- 分页控件 --> <view class="pagination-container"> <picker mode="selector" :range="['5', '10', '20', '50']" @change="handleSizeChange" class="page-size-picker" > <view class="picker"> 每页 {{ pagination.size }} 条 </view> </picker> <view class="pagination-buttons"> <button :disabled="pagination.current === 1" @click="handlePageChange(pagination.current - 1)" class="page-button prev-button" > 上一页 </button> <text class="page-info"> 第 {{ pagination.current }} 页 / 共 {{ Math.ceil(pagination.total / pagination.size) }} 页 </text> <button :disabled="pagination.current >= Math.ceil(pagination.total / pagination.size)" @click="handlePageChange(pagination.current + 1)" class="page-button next-button" > 下一页 </button> </view> <text class="total-records">共 {{ pagination.total }} 条记录</text> </view> </view> </view> </template> <style scoped> .container { padding: 20rpx; background-color: #f5f5f5; min-height: 100vh; } /* 新增样式 */ .form-row { display: flex; gap: 20rpx; margin-bottom: 30rpx; } .form-group-half { flex: 1; min-width: 0; /* 防止内容溢出 */ } /* 调整搜索选择器容器高度 */ .search-select-container { position: relative; height: 80rpx; } /* 调整搜索输入框高度 */ .search-input { height: 80rpx; } /* 调整状态选择框高度 */ .state-select-box { height: 80rpx; } /* 搜索卡片样式 */ .search-card { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); } .form-group { margin-bottom: 30rpx; } .form-label { display: block; margin-bottom: 10rpx; font-size: 28rpx; color: #606266; font-weight: 500; } .form-input, .form-picker { width: 100%; height: 80rpx; padding: 0 20rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; background-color: #fff; box-sizing: border-box; } .picker { height: 80rpx; line-height: 80rpx; color: #606266; } .button-group { display: flex; gap: 20rpx; margin-top: 20rpx; } .search-button, .reset-button { flex: 1; height: 80rpx; line-height: 80rpx; font-size: 30rpx; border-radius: 8rpx; text-align: center; } .search-button { background-color: #409eff; color: #fff; } .reset-button { background-color: #f5f7fa; color: #606266; border: 1px solid #dcdfe6; } /* 数据卡片样式 */ .data-card { background: #fff; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); } .card-header { margin-bottom: 30rpx; } .refresh-button { background-color: #409eff; color: #fff; height: 70rpx; line-height: 70rpx; font-size: 28rpx; border-radius: 8rpx; } /* 数据卡片项 */ .data-card-item { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); border: 1px solid #ebeef5; } .card-header-section { padding-bottom: 20rpx; border-bottom: 1px solid #f0f2f5; margin-bottom: 20rpx; } .card-title { font-size: 34rpx; font-weight: bold; color: #303133; line-height: 1.4; word-break: break-word; } .card-body-section { margin-bottom: 20rpx; } .card-row { display: flex; margin-bottom: 15rpx; font-size: 28rpx; } .card-label { color: #606266; width: 150rpx; } .card-value { color: #303133; flex: 1; word-break: break-word; } .card-footer-section { display: flex; justify-content: space-between; align-items: center; padding-top: 20rpx; border-top: 1px solid #f0f2f5; } .status-container { display: flex; align-items: center; gap: 20rpx; } .status-tag { padding: 8rpx 20rpx; border-radius: 40rpx; font-size: 24rpx; font-weight: 500; } .status-submitted { background-color: #f0f9eb; color: #67c23a; border: 1px solid #e1f3d8; } .status-not-submitted { background-color: #fef0f0; color: #f56c6c; border: 1px solid #fde2e2; } .score { font-size: 28rpx; color: #e6a23c; font-weight: 500; } .view-button { background-color: #409eff; color: #fff; height: 60rpx; line-height: 60rpx; padding: 0 30rpx; font-size: 26rpx; border-radius: 40rpx; } /* 分页样式 */ .pagination-container { margin-top: 40rpx; display: flex; flex-direction: column; align-items: center; gap: 20rpx; } .page-size-picker { width: 200rpx; height: 60rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; text-align: center; line-height: 60rpx; font-size: 26rpx; } .pagination-buttons { display: flex; align-items: center; gap: 20rpx; } .page-button { height: 60rpx; line-height: 60rpx; padding: 0 30rpx; font-size: 26rpx; border-radius: 8rpx; background-color: #f5f7fa; color: #606266; border: 1px solid #dcdfe; } .page-button:disabled { opacity: 0.5; } .prev-button::before { content: "← "; } .next-button::after { content: " →"; } .page-info { font-size: 26rpx; color: #606266; } .total-records { font-size: 26rpx; color: #909399; } /* 加载状态样式 */ .loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80rpx 0; } .loading-spinner { width: 80rpx; height: 80rpx; border: 8rpx solid #f3f3f3; border-top: 8rpx solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 30rpx; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-text { color: #666; font-size: 28rpx; } /* 空数据提示 */ .empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 32rpx; } /* 新增的搜索选择器样式 */ .search-select-container { position: relative; width: 100%; } .search-input { width: 100%; height: 80rpx; padding: 0 60rpx 0 20rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; background-color: #fff; box-sizing: border-box; } .dropdown-icon { position: absolute; right: 20rpx; top: 50%; transform: translateY(-50%); pointer-events: none; font-size: 24rpx; color: #606266; } .dropdown-list { position: absolute; top: 100%; left: 0; right: 0; max-height: 400rpx; background: #fff; border: 1px solid #dcdfe6; border-radius: 8rpx; z-index: 1000; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); margin-top: 10rpx; } .dropdown-scroll { max-height: 400rpx; } .dropdown-item { padding: 20rpx; font-size: 28rpx; color: #333; border-bottom: 1px solid #f0f2f5; } .dropdown-item.selected { background-color: #f5f7fa; color: #409eff; font-weight: 500; } .dropdown-item:last-child { border-bottom: none; } .dropdown-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: transparent; z-index: 999; } .empty-option { padding: 20rpx; text-align: center; color: #999; font-size: 28rpx; } .selected-display { position: absolute; top: 0; left: 0; right: 60rpx; height: 80rpx; padding: 0 20rpx; line-height: 80rpx; color: #303133; pointer-events: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* 状态选择器样式 */ .state-select-box { position: relative; width: 100%; height: 80rpx; padding: 0 60rpx 0 20rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; background-color: #fff; display: flex; align-items: center; box-sizing: border-box; } .selected-state { color: #303133; } .state-dropdown { max-height: 300rpx; } </style> 提交状态下拉选择宽并没有和按钮组在同一行
07-24
<script setup> import { ref, reactive, onMounted, computed } from 'vue'; import { onShow } from '@dcloudio/uni-app'; // 响应式数据 const surveyList = ref([]); const total = ref(0); const loading = ref(false); const formData = reactive({ dcWjTitle: '', dcId: [], dcDept: '', state: null }); const bdrOptions = ref([]); const bdrLoading = ref(false); const pagination = reactive({ current: 1, size: 10, total: 0 }); // 新增的搜索选择器相关变量 const bdrSearchKeyword = ref(''); const showBdrList = ref(false); const filteredBdrOptions = ref([]); // 计算选中的被测评人标签 const selectedBdrLabel = computed(() => { if (formData.dcId.length === 0) return ''; const selected = bdrOptions.value.find(b => b.value === formData.dcId[0]); return selected ? selected.label : ''; }); // 过滤选项 const filterBdrOptions = () => { if (!bdrSearchKeyword.value) { filteredBdrOptions.value = [...bdrOptions.value]; return; } const keyword = bdrSearchKeyword.value.toLowerCase(); filteredBdrOptions.value = bdrOptions.value.filter(item => item.label.toLowerCase().includes(keyword) ); }; // 选择被测评人 const handleBdrSelect = (item) => { formData.dcId = [item.value]; showBdrList.value = false; bdrSearchKeyword.value = ''; }; // 切换下拉列表显示 const toggleBdrList = () => { showBdrList.value = !showBdrList.value; if (showBdrList.value) { filterBdrOptions(); } }; // 获取问卷数据 const fetchSurveyData = async () => { try { loading.value = true; // 获取 token const token = uni.getStorageSync('token'); if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); uni.redirectTo({ url: '/pages/login/login' }); return; } // 准备请求参数 const params = { pageNum: pagination.current, pageSize: pagination.size, dcWjTitle: formData.dcWjTitle, dcId: formData.dcId.join(','), dcDept: formData.dcDept, state: formData.state }; // 发送请求 const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/listTx', method: 'GET', data: params, header: { 'Authorization': `Bearer ${token}` } }); // 错误处理 if (res.statusCode !== 200) { throw new Error(`请求失败,状态码: ${res.statusCode}`); } // 处理响应数据 const response = res.data; if (response.code === 200) { surveyList.value = response.rows || []; total.value = response.total || 0; pagination.total = response.total || 0; } else { throw new Error(response.msg || 'API返回错误'); } } catch (error) { console.error('请求错误:', error); uni.showToast({ title: error.message || '加载失败', icon: 'none', duration: 3000 }); } finally { loading.value = false; } }; // 获取被测评人列表 const fetchBdrList = async () => { try { const token = uni.getStorageSync('token'); if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); return; } const res = await uni.request({ url: 'http://172.26.26.43/dev-api/wjdc/wj/getBdrList', method: 'GET', header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 8000 }); if (res.statusCode !== 200) { throw new Error(`HTTP错误: ${res.statusCode}`); } const response = res.data; let data = []; if (Array.isArray(response)) { data = response; } else if (response.data) { data = response.data; } else if (response.rows) { data = response.rows; } else { throw new Error('无效的API响应格式'); } if (!Array.isArray(data)) { throw new Error('响应数据格式错误'); } bdrOptions.value = data.map(item => ({ label: item.dcName || '未知名称', value: item.dcId || item.id || null })); // 初始化过滤后的选项 filteredBdrOptions.value = [...bdrOptions.value]; } catch (error) { console.error('获取被测评人失败:', error); let errorMsg = '获取被测评人失败'; if (error.message.includes('401')) { errorMsg = '认证失败,请重新登录'; uni.redirectTo({ url: '/pages/login/login' }); } else if (error.message.includes('timeout')) { errorMsg = '请求超时,请检查网络'; } else if (error.message.includes('404')) { errorMsg = 'API地址不存在'; } uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 }); } }; // 搜索按钮处理 const handleSearch = () => { pagination.current = 1; fetchSurveyData(); }; // 重置按钮处理 - 添加搜索选择器重置逻辑 const handleReset = () => { formData.dcWjTitle = ''; formData.dcId = []; formData.dcDept = ''; formData.state = null; bdrSearchKeyword.value = ''; showBdrList.value = false; handleSearch(); }; // 编辑/查看问卷 const handleView = (item) => { uni.navigateTo({ url: `/pages/operation/operation?id=${item.dcWjId}` }); }; // 分页大小改变 const handleSizeChange = (e) => { pagination.size = e.detail.value; fetchSurveyData(); }; // 页码改变 const handlePageChange = (page) => { pagination.current = page; fetchSurveyData(); }; // 刷新数据 const refreshData = () => { pagination.current = 1; fetchSurveyData(); }; // 页面显示时加载数据 onShow(() => { fetchSurveyData(); fetchBdrList(); }); </script> <template> <view class="container"> <!-- 搜索表单区域 --> <view class="search-card"> <view class="form-group"> <text class="form-label">问卷标题:</text> <input v-model="formData.dcWjTitle" placeholder="请输入问卷标题" class="form-input" /> </view> <!-- 修改后的被测评人选择器 --> <view class="form-group"> <text class="form-label">被测评人:</text> <view class="search-select-container"> <!-- 搜索输入框 --> <input v-model="bdrSearchKeyword" placeholder="搜索被测评人" class="search-input" @focus="showBdrList = true" @input="filterBdrOptions" /> <!-- 下拉列表 --> <view v-if="showBdrList" class="dropdown-list"> <scroll-view scroll-y="true" class="dropdown-scroll"> <view v-for="(item, index) in filteredBdrOptions" :key="index" class="dropdown-item" :class="{ 'selected': formData.dcId[0] === item.value }" @click="handleBdrSelect(item)" > {{ item.label }} </view> <view v-if="filteredBdrOptions.length === 0" class="empty-option"> 无匹配结果 </view> </scroll-view> </view> <!-- 已选项显示 --> <view v-if="formData.dcId.length > 0 && !showBdrList" class="selected-display"> {{ selectedBdrLabel }} </view> <!-- 下拉箭头 --> <view class="dropdown-icon" @click="toggleBdrList"> <text>▼</text> </view> <!-- 遮罩层 --> <view v-if="showBdrList" class="dropdown-mask" @click="showBdrList = false" ></view> </view> </view> <view class="form-group"> <text class="form-label">人员部门:</text> <input v-model="formData.dcDept" placeholder="请输入人员部门" class="form-input" /> </view> <view class="form-group"> <text class="form-label">提交状态:</text> <picker mode="selector" :range="['全部', '已提交', '未提交']" @change="(e) => formData.state = e.detail.value === 0 ? null : e.detail.value === 1 ? 1 : 0" class="form-picker" > <view class="picker"> {{ formData.state === null ? '全部' : formData.state === 1 ? '已提交' : '未提交' }} </view> </picker> </view> <view class="button-group"> <button class="search-button" @click="handleSearch">搜索</button> <button class="reset-button" @click="handleReset">重置</button> </view> </view> <!-- 数据显示区域 --> <view class="data-card"> <view class="card-header"> <button class="refresh-button" @click="refreshData">刷新数据</button> </view> <!-- 加载状态 --> <view v-if="loading" class="loading-container"> <view class="loading-spinner"></view> <text class="loading-text">加载中...</text> </view> <!-- 数据展示 --> <view v-else> <view v-for="(item, index) in surveyList" :key="index" class="data-card-item"> <view class="card-header-section"> <view class="card-title">{{ item.dcWjTitle }}</view> </view> <view class="card-body-section"> <view class="card-row"> <text class="card-label">被测评人:</text> <text class="card-value">{{ item.dcName }}</text> </view> <view class="card-row"> <text class="card-label">部门:</text> <text class="card-value">{{ item.dcDept }}</text> </view> <view class="card-row"> <text class="card-label">创建时间:</text> <text class="card-value">{{ item.createTime }}</text> </view> <view class="card-row"> <text class="card-label">提交时间:</text> <text class="card-value">{{ item.updateTime || '-' }}</text> </view> </view> <view class="card-footer-section"> <view class="status-container"> <view :class="['status-tag', item.state === '1' ? 'status-submitted' : 'status-not-submitted']"> {{ item.state === '1' ? '已提交' : '未提交' }} </view> <view class="score">总分: {{ item.score || '0' }}</view> </view> <button class="view-button" @click="handleView(item)">编辑/查看</button> </view> </view> <!-- 空数据提示 --> <view v-if="surveyList.length === 0" class="empty"> <text>暂无数据</text> </view> </view> <!-- 分页控件 --> <view class="pagination-container"> <picker mode="selector" :range="['5', '10', '20', '50']" @change="handleSizeChange" class="page-size-picker" > <view class="picker"> 每页 {{ pagination.size }} 条 </view> </picker> <view class="pagination-buttons"> <button :disabled="pagination.current === 1" @click="handlePageChange(pagination.current - 1)" class="page-button prev-button" > 上一页 </button> <text class="page-info"> 第 {{ pagination.current }} 页 / 共 {{ Math.ceil(pagination.total / pagination.size) }} 页 </text> <button :disabled="pagination.current >= Math.ceil(pagination.total / pagination.size)" @click="handlePageChange(pagination.current + 1)" class="page-button next-button" > 下一页 </button> </view> <text class="total-records">共 {{ pagination.total }} 条记录</text> </view> </view> </view> </template> <style scoped> .container { padding: 20rpx; background-color: #f5f5f5; min-height: 100vh; } /* 搜索卡片样式 */ .search-card { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); } .form-group { margin-bottom: 30rpx; } .form-label { display: block; margin-bottom: 10rpx; font-size: 28rpx; color: #606266; font-weight: 500; } .form-input, .form-picker { width: 100%; height: 80rpx; padding: 0 20rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; background-color: #fff; box-sizing: border-box; } .picker { height: 80rpx; line-height: 80rpx; color: #606266; } .button-group { display: flex; gap: 20rpx; margin-top: 20rpx; } .search-button, .reset-button { flex: 1; height: 80rpx; line-height: 80rpx; font-size: 30rpx; border-radius: 8rpx; text-align: center; } .search-button { background-color: #409eff; color: #fff; } .reset-button { background-color: #f5f7fa; color: #606266; border: 1px solid #dcdfe6; } /* 数据卡片样式 */ .data-card { background: #fff; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); } .card-header { margin-bottom: 30rpx; } .refresh-button { background-color: #409eff; color: #fff; height: 70rpx; line-height: 70rpx; font-size: 28rpx; border-radius: 8rpx; } /* 数据卡片项 */ .data-card-item { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); border: 1px solid #ebeef5; } .card-header-section { padding-bottom: 20rpx; border-bottom: 1px solid #f0f2f5; margin-bottom: 20rpx; } .card-title { font-size: 34rpx; font-weight: bold; color: #303133; line-height: 1.4; word-break: break-word; } .card-body-section { margin-bottom: 20rpx; } .card-row { display: flex; margin-bottom: 15rpx; font-size: 28rpx; } .card-label { color: #606266; width: 150rpx; } .card-value { color: #303133; flex: 1; word-break: break-word; } .card-footer-section { display: flex; justify-content: space-between; align-items: center; padding-top: 20rpx; border-top: 1px solid #f0f2f5; } .status-container { display: flex; align-items: center; gap: 20rpx; } .status-tag { padding: 8rpx 20rpx; border-radius: 40rpx; font-size: 24rpx; font-weight: 500; } .status-submitted { background-color: #f0f9eb; color: #67c23a; border: 1px solid #e1f3d8; } .status-not-submitted { background-color: #fef0f0; color: #f56c6c; border: 1px solid #fde2e2; } .score { font-size: 28rpx; color: #e6a23c; font-weight: 500; } .view-button { background-color: #409eff; color: #fff; height: 60rpx; line-height: 60rpx; padding: 0 30rpx; font-size: 26rpx; border-radius: 40rpx; } /* 分页样式 */ .pagination-container { margin-top: 40rpx; display: flex; flex-direction: column; align-items: center; gap: 20rpx; } .page-size-picker { width: 200rpx; height: 60rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; text-align: center; line-height: 60rpx; font-size: 26rpx; } .pagination-buttons { display: flex; align-items: center; gap: 20rpx; } .page-button { height: 60rpx; line-height: 60rpx; padding: 0 30rpx; font-size: 26rpx; border-radius: 8rpx; background-color: #f5f7fa; color: #606266; border: 1px solid #dcdfe6; } .page-button:disabled { opacity: 0.5; } .prev-button::before { content: "← "; } .next-button::after { content: " →"; } .page-info { font-size: 26rpx; color: #606266; } .total-records { font-size: 26rpx; color: #909399; } /* 加载状态样式 */ .loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80rpx 0; } .loading-spinner { width: 80rpx; height: 80rpx; border: 8rpx solid #f3f3f3; border-top: 8rpx solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 30rpx; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-text { color: #666; font-size: 28rpx; } /* 空数据提示 */ .empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 32rpx; } /* 新增的搜索选择器样式 */ .search-select-container { position: relative; width: 100%; } .search-input { width: 100%; height: 80rpx; padding: 0 60rpx 0 20rpx; border: 1px solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; background-color: #fff; box-sizing: border-box; } .dropdown-icon { position: absolute; right: 20rpx; top: 50%; transform: translateY(-50%); pointer-events: none; font-size: 24rpx; color: #606266; } .dropdown-list { position: absolute; top: 100%; left: 0; right: 0; max-height: 400rpx; background: #fff; border: 1px solid #dcdfe6; border-radius: 8rpx; z-index: 1000; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); margin-top: 10rpx; } .dropdown-scroll { max-height: 400rpx; } .dropdown-item { padding: 20rpx; font-size: 28rpx; color: #333; border-bottom: 1px solid #f0f2f5; } .dropdown-item.selected { background-color: #f5f7fa; color: #409eff; font-weight: 500; } .dropdown-item:last-child { border-bottom: none; } .dropdown-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: transparent; z-index: 999; } .empty-option { padding: 20rpx; text-align: center; color: #999; font-size: 28rpx; } .selected-display { position: absolute; top: 0; left: 0; right: 60rpx; height: 80rpx; padding: 0 20rpx; line-height: 80rpx; color: #303133; pointer-events: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } </style> 我想要修改提交状态的页面布局,是一个下拉单选框
07-24
Vue3前端登<template> <div class="home-page"> <!-- 顶部区域 --> <div class="top-bar"> <div class="location"> <span>社区所在地:</span> <!-- 这里可根据实际需求换成选择器或输入框等,简单演示用文本 --> </div> <div class="search"> <input type="text" placeholder="输入搜索关键词" /> <button class="search-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" > <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.098zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" /> </svg> </button> </div> <div class="nav-btns"> <button class="btn">主页</button> <button class="btn">登录</button> <button class="btn">注册</button> </div> </div> <!-- 主体内容区域 --> <div class="main-content"> <!-- 左侧商品分类 --> <div class="category"> <h3>商品分类词条</h3> <p> 水果 鲜花 甜品 奶茶 饮料 零食 蔬菜 肉类...... </p> </div> <!-- 中间商品活动及列表 --> <div class="goods-section"> <div class="tabs"> <button class="tab active">秒杀活动</button> <button class="tab">参团</button> <button class="tab">拼团</button> </div> <div class="goods-list"> <div class="goods-item" v-for="i in 6" :key="i"> <div class="goods-img">商品图片</div> <p class="goods-name">商品名称</p> <p class="goods-price">商品价格</p> </div> </div> </div> <!-- 右侧个人相关 --> <div class="personal"> <button class="side-btn">我的页面</button> <button class="side-btn">购物车</button> </div> </div> </div> </template> <script setup> // 这里可引入需要的 Vue 组合式 API 等,当前示例简单,无需额外引入 </script> <style scoped> .home-page { width: 100%; min-height: 100vh; box-sizing: border-box; padding: 20px; } .top-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } .location span { font-size: 16px; } .search { display: flex; align-items: center; } .search input { padding: 6px 10px; border: 1px solid #ccc; border-right: none; outline: none; } .search-btn { padding: 6px 10px; border: 1px solid #ccc; background-color: #fff; cursor: pointer; } .nav-btns { display: flex; gap: 10px; } .btn { padding: 6px 12px; border: 1px solid #ccc; background-color: #fff; cursor: pointer; } .main-content { display: flex; gap: 20px; } .category { width: 200px; border: 1px solid #ccc; padding: 10px; box-sizing: border-box; } .category h3 { margin-bottom: 10px; } .goods-section { flex: 1; border: 1px solid #ccc; padding: 10px; box-sizing: border-box; } .tabs { display: flex; gap: 10px; margin-bottom: 20px; } .tab { padding: 6px 12px; border: 1px solid #ccc; background-color: #fff; cursor: pointer; } .tab.active { background-color: #f0f0f0; } .goods-list { display: flex; flex-wrap: wrap; gap: 20px; } .goods-item { width: calc(33.333% - 20px); border: 1px solid #ccc; padding: 10px; box-sizing: border-box; text-align: center; } .goods-img { border: 1px solid #ccc; padding: 40px; margin-bottom: 10px; } .personal { width: 120px; display: flex; flex-direction: column; gap: 10px; } .side-btn { padding: 6px 12px; border: 1px solid #ccc; background-color: #fff; cursor: pointer; } </style>录注册页面设计
06-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值