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