diff --git a/src/App.vue b/src/App.vue index 9c975c4..49913a0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,28 @@ \ No newline at end of file diff --git a/src/main.js b/src/main.js index efe493a..732b999 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,13 @@ import { createApp } from 'vue' +import { createPinia } from 'pinia' // 1. 引入 createPinia import App from './App.vue' -import router from './router' +import router from './router/index.js' const app = createApp(App) + +// 2. 创建 pinia 实例 +const pinia = createPinia() + app.use(router) -app.mount('#app') +app.use(pinia) // 3. 现在这里就不会报错了 +app.mount('#app') \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index bbf4ee9..204ba7d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,10 +1,15 @@ import { createRouter, createWebHistory } from 'vue-router' -const routes = [ - { path: '/', redirect: '/login' }, - { path: '/login', component: () => import('../views/Login.vue') }, +const routes =[ + // 根路径作为登录和企微回调的中转站。 + // 因为 App.vue 中没验证完时 不会渲染,所以这里给个空的 template 即可 + { path: '/', name: 'Root', component: { template: '
' } }, + { path: '/user', component: () => import('../views/UserHome.vue') }, { path: '/admin', component: () => import('../views/AdminIndex.vue') }, + + // 捕获所有未匹配的路由,重定向到根路径 + { path: '/:pathMatch(.*)*', redirect: '/' } ] const router = createRouter({ @@ -12,14 +17,22 @@ const router = createRouter({ routes, }) -// 路由守卫:校验权限 +// 路由守卫:校验权限与角色 router.beforeEach((to, from, next) => { const role = localStorage.getItem('userRole') - if (to.path !== '/login' && !role) { - next('/login') - } else { + + // 1. 如果没有角色信息,且访问的不是根路径(企微回调路径),则拦截到根路径让 App.vue 处理授权 + if (!role && to.path !== '/') { + next('/') + } + // 2. 权限越界拦截:普通用户(user)尝试访问管理端(/admin),强制拉回到用户端 + else if (role === 'user' && to.path.startsWith('/admin')) { + next('/user') + } + // 3. 正常放行 + else { next() } }) -export default router +export default router \ No newline at end of file diff --git a/src/stores/counter.ts b/src/stores/counter.ts deleted file mode 100644 index b6757ba..0000000 --- a/src/stores/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' - -export const useCounterStore = defineStore('counter', () => { - const count = ref(0) - const doubleCount = computed(() => count.value * 2) - function increment() { - count.value++ - } - - return { count, doubleCount, increment } -}) diff --git a/src/stores/user.js b/src/stores/user.js new file mode 100644 index 0000000..2414598 --- /dev/null +++ b/src/stores/user.js @@ -0,0 +1,39 @@ +import { defineStore } from 'pinia' + +export const useUserStore = defineStore('user', { + state: () => ({ + token: '', + userInfo: { + name: '', + role: '', + userid: '' + } + }), + getters: { + isAuthenticated: (state) => !!state.token && state.token.length > 0, + getUserName: (state) => state.userInfo.name, + getUserRole: (state) => state.userInfo.role, + getUserId: (state) => state.userInfo.userid + }, + actions: { + setToken(token) { + this.token = token + }, + setUserInfo(userInfo) { + this.userInfo = { + name: userInfo.name || '', + role: userInfo.role || '', + userid: userInfo.userid || '' + } + }, + setUser(token, userInfo) { + this.setToken(token) + this.setUserInfo(userInfo) + }, + logout() { + this.token = '' + this.userInfo = { name: '', role: '', userid: '' } + } + }, + persist: true +}) \ No newline at end of file diff --git a/src/utils/http.js b/src/utils/http.js new file mode 100644 index 0000000..b7d3778 --- /dev/null +++ b/src/utils/http.js @@ -0,0 +1,106 @@ +import axios from 'axios' + +// 1. 创建 axios 实例 +const service = axios.create({ + baseURL: 'https://superdata.nycjy.cn/api' || '', + timeout: 150000, + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, +}) + +// 2. 请求拦截器 +service.interceptors.request.use( + (config) => { + // 【规范】:统一使用 'token' 作为本地存储的 Key + const token = localStorage.getItem('token') + + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + console.error('Request Error:', error) + return Promise.reject(error) + }, +) + +// 3. 响应拦截器 +service.interceptors.response.use( + (response) => { + // 获取后端返回的数据 (例如: { code: 200, success: true, data: [...], pagination: {...} }) + const res = response.data + + if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { + return res + } + + // 处理后端的业务报错 (假设有 code 且不为 200 即为异常) + if (res.code && res.code !== 200) { + console.error(res.message || '系统异常') + + if (res.code === 401) { + localStorage.removeItem('token') + window.location.href = '/login' + } + return Promise.reject(new Error(res.message || 'Error')) + } + + // ✅ 【关键修复 1】:直接返回完整的 res,不要只返回 res.data + // 否则 AdminIndex.vue 里的 if (res.success) 会永远是 undefined 导致列表渲染失败 + return res + }, + (error) => { + // ✅ 【关键修复 2】:静默处理浏览器/Vue Router 中止引起的被取消的请求,防止弹窗报错 + if (axios.isCancel(error) || error.message === 'canceled') { + console.log('请求被中止(canceled),已静默拦截:', error.message) + return Promise.reject(error) + } + + // HTTP 网络错误统一处理 + const status = error?.response?.status // 使用可选链,防止空指针 + let errorMessage = '网络请求异常' + + switch (status) { + case 400: errorMessage = '请求参数错误'; break; + case 401: + errorMessage = '未授权,请重新登录'; + localStorage.removeItem('token'); + window.location.href = '/login'; + break; + case 403: errorMessage = '拒绝访问'; break; + case 404: errorMessage = '请求的资源不存在'; break; + case 408: errorMessage = '请求超时'; break; + case 500: errorMessage = '服务器内部错误'; break; + case 502: errorMessage = '网关错误'; break; + case 503: errorMessage = '服务不可用'; break; + case 504: errorMessage = '网关超时'; break; + default: errorMessage = `连接错误 (${status || '未知'})`; + } + + console.error(errorMessage) + return Promise.reject(error) + }, +) + +// 4. 导出常用请求方法封装 +const http = { + get(url, params, config) { + return service.get(url, { params, ...config }) + }, + post(url, data, config) { + return service.post(url, data, config) + }, + put(url, data, config) { + return service.put(url, data, config) + }, + delete(url, params, config) { + return service.delete(url, { params, ...config }) + }, + download(url, params) { + return service.get(url, { params, responseType: 'blob' }) + }, +} + +export default http \ No newline at end of file diff --git a/src/utils/request.js b/src/utils/request.js deleted file mode 100644 index b802e1e..0000000 --- a/src/utils/request.js +++ /dev/null @@ -1,129 +0,0 @@ -import axios from 'axios' - -// 1. 创建 axios 实例 -const service = axios.create({ - // 根据不同的环境使用不同的 baseURL (假设使用了 Vite 或 Webpack) - baseURL: 'http://192.168.15.115:5636/api' || '', - timeout: 10000, // 请求超时时间:10秒 - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, -}) - -// 2. 请求拦截器 -service.interceptors.request.use( - (config) => { - // 从 localStorage 或状态管理器中获取 Token - const token = localStorage.getItem('token') - - // 如果有 token,将其添加到请求头中 - if (token) { - config.headers.Authorization = `Bearer ${token}` - } - - // 你可以在这里添加全局的 Loading 效果开启逻辑 - - return config - }, - (error) => { - // 处理请求错误 - console.error('Request Error:', error) - return Promise.reject(error) - }, -) - -// 3. 响应拦截器 -service.interceptors.response.use( - (response) => { - // 获取后端返回的数据 - const res = response.data - - // 处理二进制数据 (比如下载文件) - if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { - return res - } - - // 与后端约定的业务成功状态码,这里假设是 200 - if (res.code !== 200) { - console.error(res.message || '系统异常') - - // 比如 401: Token 过期,需要重新登录 - if (res.code === 401) { - localStorage.removeItem('token') - window.location.href = '/login' // 跳回登录页 - } - - // 返回一个被拒绝的 Promise,中断调用链 - return Promise.reject(new Error(res.message || 'Error')) - } - - // 业务正常,直接剥离最外层,返回 data 里面的数据 - return res.data - }, - (error) => { - // HTTP 网络错误统一处理 - const status = error.response?.status - let errorMessage = '网络请求异常' - - switch (status) { - case 400: - errorMessage = '请求参数错误' - break - case 401: - errorMessage = '未授权,请重新登录' - break - case 403: - errorMessage = '拒绝访问' - break - case 404: - errorMessage = '请求的资源不存在' - break - case 408: - errorMessage = '请求超时' - break - case 500: - errorMessage = '服务器内部错误' - break - case 502: - errorMessage = '网关错误' - break - case 503: - errorMessage = '服务不可用' - break - case 504: - errorMessage = '网关超时' - break - default: - errorMessage = `连接错误 (${status})` - } - - console.error(errorMessage) - - return Promise.reject(error) - }, -) - -// 4. 导出常用请求方法封装 -const http = { - get(url, params, config) { - return service.get(url, { params, ...config }) - }, - - post(url, data, config) { - return service.post(url, data, config) - }, - - put(url, data, config) { - return service.put(url, data, config) - }, - - delete(url, params, config) { - return service.delete(url, { params, ...config }) - }, - - download(url, params) { - return service.get(url, { params, responseType: 'blob' }) - }, -} - -export default http diff --git a/src/views/AdminIndex.vue b/src/views/AdminIndex.vue index 94e2e1c..c605b75 100644 --- a/src/views/AdminIndex.vue +++ b/src/views/AdminIndex.vue @@ -1,205 +1,230 @@ \ No newline at end of file diff --git a/src/views/Login.vue b/src/views/Login.vue deleted file mode 100644 index 26b46db..0000000 --- a/src/views/Login.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/views/UserHome.vue b/src/views/UserHome.vue index 1d0ed9d..ba8b2d0 100644 --- a/src/views/UserHome.vue +++ b/src/views/UserHome.vue @@ -1,216 +1,179 @@ @@ -230,17 +193,29 @@ import { CreateOutline, CloseOutline, CheckmarkCircleOutline } from '@vicons/ionicons5' -import http from '@/utils/request' +import http from '@/utils/http' + +/** + * 主题定制:优化圆角和品牌色 + */ +const themeOverrides = { + common: { + primaryColor: '#18a058', + primaryColorHover: '#36ad6a', + primaryColorPressed: '#0c7a43', + borderRadius: '10px' + } +} const message = useMessage() const route = useRoute() const router = useRouter() const formRef = ref(null) -// 从路由中获取 wecom_id,后续提交时需要 +// 保持原逻辑:获取 wecom_id const wecomId = route.query.wecom_id || 'wmcr-ECwAAzKclEfIKNcVgOdxD-TcqLg' -// 响应式表单数据 +// 保持原逻辑:响应式表单数据 const formData = reactive({ analystSupervisor: '', analystDepartment: '', @@ -252,18 +227,17 @@ const formData = reactive({ transactionAmount: null, guidancePeriod: '', analystNotes: '', - // 存储 UI 显示用的文件列表 documentFileList: [], paymentFileList: [], signatureFileList: [] }) -// 临时文件列表,用于上传前预览 +// 保持原逻辑:文件预览列表 const documentFileListLast = ref([]) const paymentFileListLast = ref([]) const signatureFileListLast = ref([]) -// 定义校验规则 +// 保持原逻辑:校验规则 const rules = { analystSupervisor: { required: true, message: '请输入主管姓名', trigger: 'blur' }, parentName: { required: true, message: '请输入家长姓名', trigger: 'blur' }, @@ -271,9 +245,7 @@ const rules = { } /** - * 格式化日期:将时间戳转换为 YYYY-MM-DD 格式 - * @param {number} timestamp - The date timestamp. - * @returns {string|null} - Formatted date string or null. + * 保持原逻辑:格式化日期 */ const formatDate = (timestamp) => { if (!timestamp) return null; @@ -284,17 +256,14 @@ const formatDate = (timestamp) => { return `${year}-${month}-${day}`; }; - /** - * 核心:通用文件上传逻辑 - * 直接把后端返回的数据挂载到文件对象的 rawResponse 字段,完全隔离前端生成的 id/fullPath + * 保持原逻辑:通用文件上传逻辑 */ const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => { try { const uploadData = new FormData() uploadData.append('file', file.file) - // 注意:这里的上传接口地址可能需要根据您的实际情况修改 const response = await http.post('/v1/material/upload', uploadData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (p) => onProgress({ percent: Math.ceil((p.loaded / p.total) * 100) }) @@ -325,14 +294,12 @@ const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) } /** - * 核心:初始化数据回显 + * 保持原逻辑:初始化数据回显 */ const initData = async () => { try { - // 注意:这里的获取信息接口地址可能需要根据您的实际情况修改 const res = await http.get('/v1/customer/get_customers_info', { wecom_id: wecomId }) - // 1. 回显基础字段 formData.analystName = res.analystName formData.parentName = res.customerName formData.parentPhone = res.customerPhone @@ -342,7 +309,6 @@ const initData = async () => { if (res.dealAmount) formData.transactionAmount = Number(res.dealAmount) if (res.dealDate) formData.transactionDate = new Date(res.dealDate).getTime() - // 2. 回显付款截图 if (res.proofOfPayment && res.proofOfPayment_urls) { const paymentFiles = res.proofOfPayment.map((objName, index) => ({ id: objName, @@ -350,8 +316,10 @@ const initData = async () => { status: 'finished', url: res.proofOfPayment_urls[index], rawResponse: { - object_name: objName, - url: res.proofOfPayment_urls[index], + data: { + object_name: objName, + url: res.proofOfPayment_urls[index], + }, success: true } })) @@ -369,19 +337,17 @@ onMounted(() => { initData() }) -// 预览 const handlePreview = (file) => { const url = file.url || file.thumbnailUrl if (url) window.open(url) } -// ============================================================================== -// 核心改动: 更新提交逻辑以匹配新接口 -// ============================================================================== +/** + * 保持原逻辑:更新提交逻辑 + */ const handleSubmit = () => { formRef.value?.validate(async (errors) => { if (!errors) { - // 1. 构建完全符合后端接口要求的 payload const submitPayload = { wecom_id: wecomId, analyst_supervisor: formData.analystSupervisor, @@ -390,27 +356,16 @@ const handleSubmit = () => { parent_name: formData.parentName, parent_phone: formData.parentPhone, parent_id_card: formData.parentIdCard, - // 格式化日期和金额 transaction_date: formatDate(formData.transactionDate), transaction_amount: String(formData.transactionAmount || ''), guidance_period: formData.guidancePeriod, - // 提取文件 object_name - payment_object_names: formData.paymentFileList.map(f => f.object_name).filter(Boolean), - signature_object_name: formData.signatureFileList[0]?.object_name || null, - attachment_object_name: formData.documentFileList[0]?.object_name || null, - // analystNotes 似乎不在接口中,如果需要提交请后端添加字段 - // assignee_name 和 assignee_phone 表单中未提供,暂不提交 + payment_object_names: formData.paymentFileList.map(f => f.rawResponse?.data?.object_name).filter(Boolean), + signature_object_name: formData.signatureFileList[0]?.rawResponse?.data?.object_name || null, + attachment_object_name: formData.documentFileList[0]?.rawResponse?.data?.object_name || null, }; - - console.log('--- 准备提交给后端的最终纯净数据 ---', submitPayload); - try { - // 2. 调用新的提交接口 - // 注意:请确保 http util 配置了正确的 baseURL, 例如 http://192.168.15.115:5636/api await http.post('/v1/material/submit', submitPayload); message.success('材料已正式提交成功!'); - // 可选:提交成功后跳转或重置表单 - // router.push('/success-page'); } catch (err) { console.error('提交失败:', err); message.error('提交失败,请联系管理员'); @@ -421,256 +376,270 @@ const handleSubmit = () => { }); }; - const logout = () => router.push('/login') const handleCancel = () => router.back() - \ No newline at end of file