9 changed files with 1195 additions and 8 deletions
@ -0,0 +1,90 @@ |
|||||
|
import { createRouter, createWebHistory } from 'vue-router' |
||||
|
|
||||
|
// 设备检测函数
|
||||
|
function isMobileDevice() { |
||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) |
||||
|
} |
||||
|
|
||||
|
// 检查登录状态
|
||||
|
function isLoggedIn() { |
||||
|
const token = localStorage.getItem('token') |
||||
|
return !!token && token !== 'null' && token !== 'undefined' |
||||
|
} |
||||
|
|
||||
|
// 获取用户信息
|
||||
|
function getUserInfo() { |
||||
|
try { |
||||
|
return JSON.parse(localStorage.getItem('userInfo') || '{}') |
||||
|
} catch { |
||||
|
return {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const mobileRoutes = [ |
||||
|
{ |
||||
|
path: '/mobile/login', |
||||
|
name: 'MobileLogin', |
||||
|
component: () => import('/@/views/mobile/login.vue') |
||||
|
}, |
||||
|
{ |
||||
|
path: '/mobile/agreement', |
||||
|
name: 'MobileAgreement', |
||||
|
component: () => import('/@/views/mobile/agreement.vue'), |
||||
|
meta: { requiresAuth: true, mobileOnly: true } |
||||
|
}, |
||||
|
{ |
||||
|
path: '/mobile/service', |
||||
|
name: 'MobileService', |
||||
|
component: () => import('/@/views/mobile/index.vue'), |
||||
|
meta: { requiresAuth: true, mobileOnly: true } |
||||
|
}, |
||||
|
{ |
||||
|
path: '/mobile/service/create', |
||||
|
name: 'MobileServiceCreate', |
||||
|
component: () => import('/@/views/mobile/service/create.vue'), |
||||
|
meta: { requiresAuth: true, mobileOnly: true } |
||||
|
}, |
||||
|
{ |
||||
|
path: '/mobile', |
||||
|
redirect: '/mobile/login' |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
// 移动端路由守卫函数
|
||||
|
function mobileRouteGuard(to, from, next) { |
||||
|
if (to.path.startsWith('/mobile')) { |
||||
|
// 设备检测(开发环境暂时跳过)
|
||||
|
if (process.env.NODE_ENV === 'development') { |
||||
|
// 开发环境跳过设备检测,方便测试
|
||||
|
} else if (!isMobileDevice()) { |
||||
|
next('/') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 登录检查
|
||||
|
if (to.meta.requiresAuth && !isLoggedIn()) { |
||||
|
next('/mobile/login') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 已登录用户的智能跳转
|
||||
|
if (isLoggedIn() && to.path === '/mobile/login') { |
||||
|
const userInfo = getUserInfo() |
||||
|
const isUserOrCto = ['user', 'cto'].includes((userInfo.roleCode || '').toLowerCase()) |
||||
|
const isSigned = userInfo.agreementSignFlag === true |
||||
|
|
||||
|
if (isUserOrCto && !isSigned) { |
||||
|
next('/mobile/agreement') |
||||
|
} else { |
||||
|
next('/mobile/service') |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
next() |
||||
|
} |
||||
|
|
||||
|
// 导出移动端路由数组和守卫函数
|
||||
|
export const mobileRouters = mobileRoutes |
||||
|
export { mobileRouteGuard } |
||||
@ -0,0 +1,131 @@ |
|||||
|
<template> |
||||
|
<div class="mobile-agreement"> |
||||
|
<header class="agreement-header"> |
||||
|
<h2>承诺书签署</h2> |
||||
|
</header> |
||||
|
|
||||
|
<div class="agreement-content"> |
||||
|
<div class="agreement-text"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="agreement-actions"> |
||||
|
<button @click="handleAgree" :disabled="loading" class="agree-btn"> |
||||
|
{{ loading ? '签署中...' : '同意并继续' }} |
||||
|
</button> |
||||
|
<button @click="handleCancel" class="cancel-btn"> |
||||
|
不同意 |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref } from 'vue' |
||||
|
import { useRouter } from 'vue-router' |
||||
|
import { letterApi } from '/@/api/business/letter/letter-api' |
||||
|
import { message } from 'ant-design-vue' |
||||
|
|
||||
|
const router = useRouter() |
||||
|
const loading = ref(false) |
||||
|
|
||||
|
// 同意并继续 |
||||
|
async function handleAgree() { |
||||
|
loading.value = true |
||||
|
|
||||
|
try { |
||||
|
await letterApi.add() |
||||
|
message.success('承诺书签署成功') |
||||
|
|
||||
|
// 签署成功后刷新页面,重新获取用户信息 |
||||
|
setTimeout(() => { |
||||
|
window.location.reload() |
||||
|
}, 500) |
||||
|
} catch (error) { |
||||
|
console.error('签署失败:', error) |
||||
|
message.error('签署失败,请稍后重试') |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 不同意 |
||||
|
function handleCancel() { |
||||
|
// 不同意则退出登录 |
||||
|
router.push('/mobile/login') |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.mobile-agreement { |
||||
|
padding: 20px; |
||||
|
min-height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.agreement-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 20px; |
||||
|
padding-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.agreement-header h2 { |
||||
|
font-size: 20px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.agreement-content { |
||||
|
flex: 1; |
||||
|
background: white; |
||||
|
padding: 20px; |
||||
|
border-radius: 8px; |
||||
|
margin-bottom: 20px; |
||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.agreement-text h3 { |
||||
|
text-align: center; |
||||
|
margin-bottom: 20px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.agreement-text p { |
||||
|
margin-bottom: 15px; |
||||
|
line-height: 1.6; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.agreement-actions { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.agree-btn { |
||||
|
padding: 15px; |
||||
|
background: #1890ff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.agree-btn:disabled { |
||||
|
background: #ccc; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.cancel-btn { |
||||
|
padding: 15px; |
||||
|
background: #f5f5f5; |
||||
|
color: #666; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,373 @@ |
|||||
|
<template> |
||||
|
<div class="mobile-home"> |
||||
|
<!-- 页面头部 --> |
||||
|
<header class="home-header"> |
||||
|
<h2>服务申报</h2> |
||||
|
<div class="user-info"> |
||||
|
<span>{{ userInfo.actualName }}</span> |
||||
|
<button @click="handleLogout" class="logout-btn">退出</button> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<!-- 主要内容 --> |
||||
|
<div class="home-content"> |
||||
|
<!-- 新建申报按钮:非协会角色且非CEO角色显示(与PC端相同逻辑) --> |
||||
|
<button v-if="!isAssociationRole && !isCeo" @click="handleCreate" class="create-btn"> |
||||
|
+ 新建申报 |
||||
|
</button> |
||||
|
|
||||
|
<!-- 服务申报列表 --> |
||||
|
<div class="service-section"> |
||||
|
<h3>服务申报列表</h3> |
||||
|
|
||||
|
<div v-if="loading" class="loading"> |
||||
|
加载中... |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="service-list"> |
||||
|
<div v-for="item in serviceList" :key="item.applicationId" class="service-item" @click="viewDetail(item)"> |
||||
|
<div class="item-main"> |
||||
|
<div class="item-title">{{ item.activityName || '服务申报' }}</div> |
||||
|
<div class="item-meta"> |
||||
|
<div class="item-duration">{{ item.serviceDuration }}小时</div> |
||||
|
<div class="item-status" :class="getStatusClass(item.firmAuditStatus)"> |
||||
|
{{ getStatusText(item.firmAuditStatus) }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="item-time"> |
||||
|
{{ formatTime(item.serviceStart) }} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="serviceList.length === 0" class="empty-state"> |
||||
|
<div class="empty-icon">📋</div> |
||||
|
<div class="empty-text">暂无申报记录</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref, onMounted } from 'vue' |
||||
|
import { useRouter } from 'vue-router' |
||||
|
import { loginApi } from '/@/api/system/login-api' |
||||
|
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api' |
||||
|
import { message } from 'ant-design-vue' |
||||
|
|
||||
|
const router = useRouter() |
||||
|
const userInfo = ref({}) |
||||
|
const serviceList = ref([]) |
||||
|
const loading = ref(false) |
||||
|
|
||||
|
// 角色判断(与PC端相同逻辑) |
||||
|
const isCeo = ref(false) |
||||
|
const isAssociationRole = ref(false) |
||||
|
|
||||
|
// 获取用户信息 |
||||
|
async function getUserInfo() { |
||||
|
try { |
||||
|
const res = await loginApi.getLoginInfo() |
||||
|
userInfo.value = res.data |
||||
|
checkUserRole() // 获取登录信息后检查用户角色 |
||||
|
} catch (error) { |
||||
|
console.error('获取用户信息失败:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查用户角色(与PC端相同逻辑) |
||||
|
function checkUserRole() { |
||||
|
if (userInfo.value) { |
||||
|
const userRole = userInfo.value.roleCode || userInfo.value.roleName || '' |
||||
|
const roleLower = userRole.toLowerCase() |
||||
|
|
||||
|
// CEO角色判断(CEO不显示新建申报按钮) |
||||
|
isCeo.value = roleLower === 'ceo' |
||||
|
|
||||
|
// 协会角色判断 |
||||
|
isAssociationRole.value = roleLower.includes('协会') || |
||||
|
roleLower.includes('association') || |
||||
|
roleLower.includes('律协') || |
||||
|
roleLower.includes('律师协会') |
||||
|
|
||||
|
console.log('用户角色:', userRole, '是CEO:', isCeo.value, '是协会角色:', isAssociationRole.value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取服务申报列表 |
||||
|
async function getServiceList() { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const res = await serviceApplicationsApi.queryPage({ |
||||
|
pageNum: 1, |
||||
|
pageSize: 20 |
||||
|
}) |
||||
|
|
||||
|
if (res.code === 0) { |
||||
|
// 根据PC端逻辑,返回的数据结构是 res.data.list |
||||
|
serviceList.value = res.data.list || [] |
||||
|
console.log('获取到的申报列表:', serviceList.value) |
||||
|
} else { |
||||
|
message.error('获取申报列表失败') |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('获取申报列表失败:', error) |
||||
|
message.error('网络错误,请稍后重试') |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 新建申报 |
||||
|
function handleCreate() { |
||||
|
router.push('/mobile/service/create') |
||||
|
} |
||||
|
|
||||
|
// 查看详情 |
||||
|
function viewDetail(item) { |
||||
|
// 这里可以跳转到详情页面或显示详情弹窗 |
||||
|
console.log('查看申报详情:', item) |
||||
|
// router.push(`/mobile/service/detail/${item.id}`) |
||||
|
} |
||||
|
|
||||
|
// 退出登录 |
||||
|
function handleLogout() { |
||||
|
localStorage.removeItem('token') |
||||
|
localStorage.removeItem('userInfo') |
||||
|
router.push('/mobile/login') |
||||
|
} |
||||
|
|
||||
|
// 状态文本映射(与PC端保持一致) |
||||
|
function getStatusText(status) { |
||||
|
const statusMap = { |
||||
|
0: '待审核', |
||||
|
1: '审核通过', |
||||
|
2: '审核拒绝', |
||||
|
3: '已完成', |
||||
|
4: '草稿' |
||||
|
} |
||||
|
return statusMap[status] || '未知状态' |
||||
|
} |
||||
|
|
||||
|
// 状态样式类(与PC端保持一致) |
||||
|
function getStatusClass(status) { |
||||
|
const classMap = { |
||||
|
0: 'status-pending', |
||||
|
1: 'status-approved', |
||||
|
2: 'status-rejected', |
||||
|
3: 'status-completed', |
||||
|
4: 'status-draft' |
||||
|
} |
||||
|
return classMap[status] || 'status-unknown' |
||||
|
} |
||||
|
|
||||
|
// 时间格式化 |
||||
|
function formatTime(time) { |
||||
|
if (!time) return '' |
||||
|
return time.split(' ')[0] // 只显示日期部分 |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
getUserInfo() |
||||
|
getServiceList() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.mobile-home { |
||||
|
min-height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
} |
||||
|
|
||||
|
.home-header { |
||||
|
position: sticky; |
||||
|
top: 0; |
||||
|
background: white; |
||||
|
padding: 15px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
border-bottom: 1px solid #e8e8e8; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
|
||||
|
.home-header h2 { |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.logout-btn { |
||||
|
padding: 6px 12px; |
||||
|
background: #f5f5f5; |
||||
|
color: #666; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.home-content { |
||||
|
padding: 15px; |
||||
|
} |
||||
|
|
||||
|
.create-btn { |
||||
|
width: 100%; |
||||
|
padding: 15px; |
||||
|
background: #1890ff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
margin-bottom: 20px; |
||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3); |
||||
|
} |
||||
|
|
||||
|
.service-section { |
||||
|
background: white; |
||||
|
border-radius: 8px; |
||||
|
padding: 15px; |
||||
|
margin-top:10px; |
||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
||||
|
min-height:200px; |
||||
|
} |
||||
|
|
||||
|
.service-section h3 { |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
margin: 0 0 15px 0; |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
.loading { |
||||
|
text-align: center; |
||||
|
padding: 20px; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.service-item { |
||||
|
padding: 15px 0; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.service-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.item-main { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.item-title { |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
margin-bottom: 5px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.item-status { |
||||
|
font-size: 12px; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 12px; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
.status-pending { |
||||
|
background: #fff7e6; |
||||
|
color: #fa8c16; |
||||
|
} |
||||
|
|
||||
|
.status-approved { |
||||
|
background: #f6ffed; |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
.status-rejected { |
||||
|
background: #fff2f0; |
||||
|
color: #ff4d4f; |
||||
|
} |
||||
|
|
||||
|
.status-completed { |
||||
|
background: #f0f5ff; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.status-draft { |
||||
|
background: #f5f5f5; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.item-meta { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.item-time { |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.item-arrow { |
||||
|
color: #ccc; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
text-align: center; |
||||
|
padding: 40px 20px; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 48px; |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.empty-text { |
||||
|
color: #666; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.empty-btn { |
||||
|
padding: 10px 20px; |
||||
|
background: #1890ff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
/* 移动端优化 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.home-header { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.home-content { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.service-section { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.service-item { |
||||
|
padding: 12px 0; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,324 @@ |
|||||
|
<template> |
||||
|
<div class="mobile-login"> |
||||
|
<div class="login-header"> |
||||
|
<h2>合肥市律师公益法律服务</h2> |
||||
|
<p>管理系统</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="login-form"> |
||||
|
<div class="form-group"> |
||||
|
<label>用户名</label> |
||||
|
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label>密码</label> |
||||
|
<a-input-password |
||||
|
v-model:value="loginForm.password" |
||||
|
autocomplete="on" |
||||
|
placeholder="请输入密码" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group captcha-group"> |
||||
|
<label>验证码</label> |
||||
|
<div class="captcha-container"> |
||||
|
<a-input |
||||
|
class="captcha-input" |
||||
|
v-model:value.trim="loginForm.captchaCode" |
||||
|
placeholder="请输入验证码" |
||||
|
/> |
||||
|
<img |
||||
|
class="captcha-img" |
||||
|
:src="captchaBase64Image" |
||||
|
@click="getCaptcha" |
||||
|
alt="验证码" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox> |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
type="button" |
||||
|
@click="onLogin" |
||||
|
:disabled="loading" |
||||
|
class="login-btn" |
||||
|
> |
||||
|
{{ loading ? '登录中...' : '登录' }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="error" class="error-message"> |
||||
|
{{ error }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref, reactive, onMounted } from 'vue' |
||||
|
import { useRouter } from 'vue-router' |
||||
|
import { loginApi } from '/@/api/system/login-api' |
||||
|
import { message } from 'ant-design-vue' |
||||
|
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const' |
||||
|
import { encryptData } from '/@/lib/encrypt' |
||||
|
import { SmartLoading } from '/@/components/framework/smart-loading'; |
||||
|
import { smartSentry } from '/@/lib/smart-sentry'; |
||||
|
import { buildRoutes } from '/@/router/index'; |
||||
|
|
||||
|
import { localSave } from '/@/utils/local-util.js'; |
||||
|
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js'; |
||||
|
import { useUserStore } from '/@/store/modules/system/user'; |
||||
|
import { dictApi } from '/@/api/support/dict-api.js'; |
||||
|
import { useDictStore } from '/@/store/modules/system/dict.js'; |
||||
|
|
||||
|
const router = useRouter() |
||||
|
const loading = ref(false) |
||||
|
const error = ref('') |
||||
|
const captchaBase64Image = ref('') |
||||
|
const rememberPwd = ref(false) |
||||
|
|
||||
|
// 登录表单(与PC端保持一致) |
||||
|
const loginForm = reactive({ |
||||
|
loginName: '', |
||||
|
password: '', |
||||
|
captchaCode: '', |
||||
|
captchaUuid: '', |
||||
|
loginDevice: LOGIN_DEVICE_ENUM.H5.value, // 移动端设备标识(H5) |
||||
|
}) |
||||
|
|
||||
|
// 表单验证规则(与PC端保持一致) |
||||
|
const rules = { |
||||
|
loginName: [{ required: true, message: '用户名不能为空' }], |
||||
|
password: [{ required: true, message: '密码不能为空' }], |
||||
|
captchaCode: [{ required: true, message: '验证码不能为空' }], |
||||
|
} |
||||
|
|
||||
|
// 获取验证码 |
||||
|
async function getCaptcha() { |
||||
|
try { |
||||
|
const res = await loginApi.getCaptcha() |
||||
|
if (res.code === 0) { |
||||
|
captchaBase64Image.value = res.data.captchaBase64Image |
||||
|
loginForm.captchaUuid = res.data.captchaUuid |
||||
|
} else { |
||||
|
message.error('获取验证码失败') |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error('获取验证码错误:', err) |
||||
|
message.error('获取验证码失败,请稍后重试') |
||||
|
} |
||||
|
} |
||||
|
let refreshCaptchaInterval = null; |
||||
|
function beginRefreshCaptchaInterval(expireSeconds) { |
||||
|
if (refreshCaptchaInterval === null) { |
||||
|
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function stopRefreshCaptchaInterval() { |
||||
|
if (refreshCaptchaInterval != null) { |
||||
|
clearInterval(refreshCaptchaInterval); |
||||
|
refreshCaptchaInterval = null; |
||||
|
} |
||||
|
} |
||||
|
// 表单验证 |
||||
|
function validateForm() { |
||||
|
if (!loginForm.loginName.trim()) { |
||||
|
message.error('请输入用户名') |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if (!loginForm.password) { |
||||
|
message.error('请输入密码') |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if (!loginForm.captchaCode.trim()) { |
||||
|
message.error('请输入验证码') |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//登录 |
||||
|
async function onLogin() { |
||||
|
|
||||
|
try { |
||||
|
SmartLoading.show(); |
||||
|
// 密码加密 |
||||
|
let encryptPasswordForm = Object.assign({}, loginForm, { |
||||
|
password: encryptData(loginForm.password), |
||||
|
}); |
||||
|
const res = await loginApi.login(encryptPasswordForm); |
||||
|
stopRefreshCaptchaInterval(); |
||||
|
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : ''); |
||||
|
message.success('登录成功'); |
||||
|
//更新用户信息到pinia |
||||
|
useUserStore().setUserLoginInfo(res.data); |
||||
|
// 初始化数据字典 |
||||
|
const dictRes = await dictApi.getAllDictData(); |
||||
|
useDictStore().initData(dictRes.data); |
||||
|
//构建系统的路由 |
||||
|
buildRoutes(); |
||||
|
router.push('/mobile/service'); |
||||
|
} catch (e) { |
||||
|
if (e.data && e.data.code !== 0) { |
||||
|
loginForm.captchaCode = ''; |
||||
|
getCaptcha(); |
||||
|
} |
||||
|
smartSentry.captureError(e); |
||||
|
} finally { |
||||
|
SmartLoading.hide(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// 回车键登录 |
||||
|
function handleKeyup(event) { |
||||
|
if (event.keyCode === 13) { |
||||
|
onLogin() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
// 绑定回车键事件 |
||||
|
document.addEventListener('keyup', handleKeyup) |
||||
|
|
||||
|
// 初始化验证码 |
||||
|
getCaptcha() |
||||
|
|
||||
|
// 如果有记住的用户名,自动填充 |
||||
|
const rememberedLoginName = localStorage.getItem('rememberedLoginName') |
||||
|
if (rememberedLoginName) { |
||||
|
loginForm.loginName = rememberedLoginName |
||||
|
rememberPwd.value = true |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.mobile-login { |
||||
|
padding: 20px; |
||||
|
min-height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
} |
||||
|
|
||||
|
.login-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 40px; |
||||
|
padding-top: 60px; |
||||
|
} |
||||
|
|
||||
|
.login-header h2 { |
||||
|
font-size: 24px; |
||||
|
color: #333; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.login-header p { |
||||
|
color: #666; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.login-form { |
||||
|
background: white; |
||||
|
padding: 30px 20px; |
||||
|
border-radius: 8px; |
||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.form-group label { |
||||
|
display: block; |
||||
|
margin-bottom: 8px; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.captcha-group { |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.captcha-container { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.captcha-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.captcha-img { |
||||
|
width: 100px; |
||||
|
height: 40px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.login-btn { |
||||
|
width: 100%; |
||||
|
padding: 12px; |
||||
|
background: #1890ff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 4px; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.login-btn:disabled { |
||||
|
background: #ccc; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.error-message { |
||||
|
margin-top: 15px; |
||||
|
padding: 10px; |
||||
|
background: #fff2f0; |
||||
|
border: 1px solid #ffccc7; |
||||
|
border-radius: 4px; |
||||
|
color: #a8071a; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 移动端优化 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.mobile-login { |
||||
|
padding: 15px; |
||||
|
} |
||||
|
|
||||
|
.login-form { |
||||
|
padding: 20px 15px; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.captcha-container { |
||||
|
/* 保持水平排列,不换行 */ |
||||
|
flex-direction: row; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.captcha-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.captcha-img { |
||||
|
width: 120px; |
||||
|
height: 44px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,231 @@ |
|||||
|
<script setup> |
||||
|
import { ref, reactive, onMounted } from 'vue' |
||||
|
import { useRouter } from 'vue-router' |
||||
|
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api' |
||||
|
import { loginApi } from '/@/api/system/login-api' |
||||
|
import { positionApi } from '/@/api/system/position-api' // 引入职务API |
||||
|
import { categoryApi } from '/@/api/business/category-api' // 引入分类API |
||||
|
import { activityApi } from '/@/api/business/activity-api' // 引入活动API |
||||
|
import { message } from 'ant-design-vue' |
||||
|
|
||||
|
const router = useRouter() |
||||
|
const loading = ref(false) |
||||
|
const readonlyMode = ref(false) |
||||
|
|
||||
|
// 表单数据(与PC端完全一致) |
||||
|
const form = reactive({ |
||||
|
actualName: '', |
||||
|
certificateNumber: '', |
||||
|
firmName: '', |
||||
|
positionId: '', |
||||
|
serviceStart: '', |
||||
|
serviceEnd: '', |
||||
|
serviceDuration: null, |
||||
|
activityCategoryId: '', |
||||
|
activityNameId: '', |
||||
|
beneficiaryCount: null, |
||||
|
organizerName: '', |
||||
|
organizerContact: '', |
||||
|
organizerPhone: '', |
||||
|
serviceContent: '', |
||||
|
proofMaterials: [] |
||||
|
}) |
||||
|
|
||||
|
// 下拉选项数据 |
||||
|
const positionList = ref([]) |
||||
|
const activityCategoryList = ref([]) |
||||
|
const activityList = ref([]) |
||||
|
const uploadedFiles = ref([]) |
||||
|
|
||||
|
// 获取用户信息并填充表单 |
||||
|
async function getUserInfo() { |
||||
|
try { |
||||
|
const res = await loginApi.getLoginInfo() |
||||
|
const userInfo = res.data |
||||
|
|
||||
|
// 填充用户信息(与PC端逻辑一致) |
||||
|
form.actualName = userInfo.actualName || '' |
||||
|
form.certificateNumber = userInfo.licenseNumber || '' |
||||
|
form.firmName = userInfo.departmentName || '' |
||||
|
} catch (error) { |
||||
|
console.error('获取用户信息失败:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取下拉选项数据 |
||||
|
async function getSelectOptions() { |
||||
|
try { |
||||
|
// 获取职务列表 - 使用现有的接口 |
||||
|
const positionRes = await positionApi.getPositionList() |
||||
|
positionList.value = positionRes.data || [] |
||||
|
|
||||
|
// 获取活动类型列表 - 使用现有的接口 |
||||
|
const categoryRes = await categoryApi.getCategoryList({ |
||||
|
categoryType: 'ACTIVITY' // 根据实际情况调整 |
||||
|
}) |
||||
|
activityCategoryList.value = categoryRes.data || [] |
||||
|
} catch (error) { |
||||
|
console.error('获取选项数据失败:', error) |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 活动类型改变事件 |
||||
|
async function onActivityCategoryChange() { |
||||
|
if (form.activityCategoryId) { |
||||
|
try { |
||||
|
// 根据活动类型获取活动名称列表 - 使用现有的接口 |
||||
|
const activityRes = await activityApi.getActivityList({ |
||||
|
categoryId: form.activityCategoryId |
||||
|
}) |
||||
|
activityList.value = activityRes.data || [] |
||||
|
} catch (error) { |
||||
|
console.error('获取活动列表失败:', error) |
||||
|
activityList.value = [] |
||||
|
} |
||||
|
} else { |
||||
|
activityList.value = [] |
||||
|
} |
||||
|
form.activityNameId = '' |
||||
|
} |
||||
|
|
||||
|
// 文件上传处理 |
||||
|
function handleFileUpload(event) { |
||||
|
const files = event.target.files |
||||
|
if (files.length + uploadedFiles.value.length > 5) { |
||||
|
message.error('最多只能上传5个文件') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
for (let file of files) { |
||||
|
if (file.size > 10 * 1024 * 1024) { |
||||
|
message.error(`文件 ${file.name} 超过10MB限制`) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
uploadedFiles.value.push({ |
||||
|
id: Date.now() + Math.random(), |
||||
|
name: file.name, |
||||
|
file: file |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 清空input |
||||
|
event.target.value = '' |
||||
|
} |
||||
|
|
||||
|
// 删除文件 |
||||
|
function removeFile(fileId) { |
||||
|
uploadedFiles.value = uploadedFiles.value.filter(file => file.id !== fileId) |
||||
|
} |
||||
|
|
||||
|
// 保存草稿 |
||||
|
async function handleSave() { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
// 调用保存草稿接口 - 使用现有的接口 |
||||
|
const res = await serviceApplicationsApi.add({ |
||||
|
...form, |
||||
|
// 添加必要的参数 |
||||
|
status: 4 // 草稿状态 |
||||
|
}) |
||||
|
|
||||
|
if (res.code === 0) { |
||||
|
message.success('保存成功') |
||||
|
router.push('/mobile/service') |
||||
|
} else { |
||||
|
message.error(res.msg || '保存失败') |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('保存失败:', error) |
||||
|
message.error('保存失败,请稍后重试') |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 提交申报 |
||||
|
async function handleSubmit() { |
||||
|
// 表单验证(与PC端相同的验证逻辑) |
||||
|
if (!form.positionId) { |
||||
|
message.error('请选择职务') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!form.serviceStart) { |
||||
|
message.error('请选择服务开始时间') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!form.serviceEnd) { |
||||
|
message.error('请选择服务结束时间') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!form.activityCategoryId) { |
||||
|
message.error('请选择活动类型') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!form.activityNameId) { |
||||
|
message.error('请选择活动名称') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!form.serviceContent) { |
||||
|
message.error('请输入服务内容描述') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (uploadedFiles.value.length === 0) { |
||||
|
message.error('请上传证明材料') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
loading.value = true |
||||
|
|
||||
|
try { |
||||
|
// 准备上传的文件 |
||||
|
const formData = new FormData() |
||||
|
|
||||
|
// 添加表单数据 |
||||
|
Object.keys(form).forEach(key => { |
||||
|
if (form[key] !== null && form[key] !== undefined) { |
||||
|
formData.append(key, form[key]) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 添加文件 |
||||
|
uploadedFiles.value.forEach(file => { |
||||
|
formData.append('files', file.file) |
||||
|
}) |
||||
|
|
||||
|
// 调用提交接口(与PC端相同的接口) |
||||
|
const res = await serviceApi.add(formData) |
||||
|
|
||||
|
if (res.code === 0) { |
||||
|
message.success('申报提交成功') |
||||
|
setTimeout(() => { |
||||
|
router.push('/mobile/service') |
||||
|
}, 1000) |
||||
|
} else { |
||||
|
message.error(res.msg || '提交失败') |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('提交失败:', error) |
||||
|
message.error('提交失败,请稍后重试') |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 返回 |
||||
|
function handleBack() { |
||||
|
router.back() |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
getUserInfo() |
||||
|
getSelectOptions() |
||||
|
}) |
||||
|
</script> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<template> |
||||
|
<div class="mobile-service"> |
||||
|
<h2>服务申报页面</h2> |
||||
|
<p>这是移动端的服务申报首页</p> |
||||
|
<router-link to="/mobile/service/create">新建申报</router-link> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
// 移动端服务申报首页 |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.mobile-service { |
||||
|
padding: 20px; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue