Commit c1b80523 by 李宁

1

1 parent 994306b9
{
"permissions": {
"allow": [
"Bash(grep -E \"\\.(js|vue|html)$\")",
"Bash(npm install axios)"
],
"deny": [],
"ask": []
}
}
\ No newline at end of file
......@@ -18,6 +18,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@tailwindcss/typography": "^0.5.19",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
"element-plus": "^2.11.7",
"lucide-vue-next": "^0.552.0",
......
import request from '../../request'
/**
* 查询账号列表
*/
export function queryAccountList(data) {
return request({
url: '/compass/api/system/account/page',
data,
})
}
/**
* 查询角色列表
*/
export function queryRoleList(data) {
return request({
url: '/compass/api/common/enums/account-roles',
method: 'GET'
})
}
/**
* 删除账号
*/
export function deleteRole(data) {
return request({
url: '/compass/api/system/account/delete',
data,
})
}
/**
* 添加/更新账号
*/
export function addAndUpdateRole(data) {
return request({
url: '/compass/api/system/account' + (data.id?'/update':'/create'),
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 根据级别查询区域列表
* 1-省级,2-市级,3-区级,4-网格级
*/
export function queryLevelAllArea(data) {
return request({
url: '/compass/api/common/areas/level?areaLevel='+data.areaLevel+'&parentAreaCode='+data.parentAreaCode,
method: 'GET'
})
}
/**
* 当前用户权限获取下级区域层级结构
*/
export function queryUserArea(data) {
return request({
url: '/compass/api/system/area/permission/hierarchy',
data,
})
}
/**
* 商机状态列表
*/
export function queryBusiStatus(data) {
return request({
url: '/compass/api/common/enums/opportunity-statuses',
method: 'GET',
data,
})
}
\ No newline at end of file
import * as account from './account'
import * as common from './common'
import * as login from './login'
import * as order from './order'
import * as reward from './reward'
import * as role from './role'
export default {
...account,
...common,
...login,
...order,
...reward,
...role
}
\ No newline at end of file
import request from '../../request'
/**
* 退出登录
*/
export function logout() {
return request({
url: '/compass/api/auth/logout',
data: {}
})
}
/**
* 手机账号登录
*/
export function pohoneLogin(data) {
return request({
url: '/crm/login',
data,
})
}
/**
* 获取图形验证码
* @param loginName
* @param password
*/
export function getImgCode(data) {
return request({
url: '/crm/getCode',
method: 'GET',
data,
})
}
/**
* 获取短信验证码
* @param loginName
* @param password
*/
export function getTelCode(data) {
return request({
url: '/crm/sendMessage',
data,
})
}
import request from '../../request'
/**
* 网格列表查询
* @returns {AxiosPromise}
*/
export function queryAllGridList(data) {
return request({
url: '/compass/api/grid/list',
data,
})
}
/**
* 获取该账号下可选的网格列表
*/
export function queryGridList(data) {
return request({
url: '/compass/api/grid/options',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 全部商机-列表查询
*/
export function queryAllBusi(data) {
return request({
url: '/compass/api/opportunity/page',
data,
})
}
/**
* 全部商机-商机审核
*/
export function audioBusi(data) {
return request({
url: '/compass/api/opportunity/audit',
data,
})
}
/**
* 全部商机-商机详情
*/
export function queryBusiDetail(data) {
return request({
url: '/compass/api/opportunity/detail',
data,
})
}
/**
* 全部商机-商机跟进记录查询
*/
export function queryBusiFollowList(data) {
return request({
url: '/compass/api/opportunity/follow/page',
data,
})
}
/**
* 全部商机-关闭商机
*/
export function closeBusi(data) {
return request({
url: '/compass/api/opportunity/close',
data,
})
}
/**
* 全部商机-标记成单
*/
export function dealBusi(data) {
return request({
url: '/compass/api/opportunity/deal',
data,
})
}
/**
* 全部商机-分配商机
*/
export function reassignBusi(data) {
return request({
url: '/compass/api/opportunity/reassign',
data,
})
}
/**
* 全部商机-记录管理员备注
*/
export function updateRemark(data) {
return request({
url: '/compass/api/opportunity/admin-remark',
data,
})
}
/**
* 全部商机-数据统计
*/
export function queryAllBusiStatistics(data) {
return request({
url: '/compass/api/opportunity/statistics',
data,
})
}
/**
* 全部商机-查询所有商机标签列表
*/
export function queryBusiLabelList(data) {
return request({
url: '/compass/api/opportunity/tag/page',
data,
})
}
/**
* 商机标签的开始和关闭
*/
export function updateBusiLabelStatus(data) {
return request({
url: '/compass/api/opportunity/tag/status',
data,
})
}
/**
* 商机标签的删除
*/
export function deleteBusiLabel(data) {
return request({
url: '/compass/api/opportunity/tag/delete',
data,
})
}
/**
* 商机标签的创建和更新
*/
export function createAndUpdateTag(data) {
return request({
url: '/compass/api/opportunity/tag' + (data.id?'/update':''),
data,
})
}
/**
* 商机关闭原因列表查询
*/
export function queryBusiCloseReansonList(data) {
return request({
url: '/compass/api/opportunity/close-reason/page',
data,
})
}
/**
* 商机关闭原因:根据区域列表查询
*/
export function queryAreaCloseReansonList(data) {
return request({
url: '/compass/api/common/close-reason/page',
data,
})
}
/**
* 商机关闭原因添加和更新
*/
export function busiCloseReasonUpdate(data) {
return request({
url: '/compass/api/opportunity/close-reason'+(data.id?'/update':''),
data,
})
}
/**
* 商机关闭原因删除
*/
export function busiCloseReasonDel(data) {
return request({
url: '/compass/api/opportunity/close-reason/delete',
data,
})
}
/**
* 全部商机-新增商机
*/
export function createBusi(data) {
return request({
url: '/compass/api/opportunity/create',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 查询人员列表
*/
export function queryAllPerson(data) {
return request({
url: '/compass/api/personnel/list',
data,
})
}
/**
* 添加人员
*/
export function addNewPerson(data) {
let url = '/compass/api/personnel/create'
if(data.id){
url = '/compass/api/personnel/update'
}
return request({
url: url,
data,
})
}
/**
* 删除人员
*/
export function deletePerson(data) {
return request({
url: '/compass/api/personnel/delete',
data,
})
}
/**
* 批量导入工维人员
*/
export function importGwPerson(data) {
return request({
url: '/compass/api/personnel/import-maintenance-preview',
data,
})
}
/**
* 批量导入营销人员
*/
export function importYxPerson(data) {
return request({
url: '/compass/api/marketing/import-preview',
data,
})
}
import axios from "axios";
import { ElMessageBox } from "element-plus";
import router from "@/router";
const service = axios.create({
baseURL: '/hallserver',
method: "post",
timeout: 150000,
withCredentials: true,
});
//请求拦截
service.interceptors.request.use(
(config) => {
if (!config.headers["Content-Type"])
config.headers["Content-Type"] = "application/json;charset=utf-8";
if (localStorage.pcUserInfo) {
let userInfo = JSON.parse(localStorage.pcUserInfo);
config.headers["x-access-token"] = userInfo.token
}
return config;
},
(error) => {
Promise.reject(error);
}
);
let ifCanShow = true; //为了防止页面有异常情况时,多个接口请求导致弹窗多次的问题
let catchFun = function (msg) {
if (!ifCanShow) {
return;
}
ifCanShow = false;
ElMessageBox.confirm(msg, "提示", {
showClose: false,
closeOnPressEscape: false,
closeOnClickModal: false,
showCancelButton: false,
}).then(() => {
ifCanShow = true;
router.push({ path: "/login" });
});
};
//响应拦截
service.interceptors.response.use(
(response) => {
console.log(response);
if (response.status == 200) {
if (response.data.code == "401") {
//登陆失效,重新登陆
catchFun("账户状态异常");
} else if (response.data.code == "133") {
//灰名单
ElMessageBox.alert(response.data.msg, "状态异常/错误提示", {
dangerouslyUseHTMLString: true,
});
} else {
if (response.data instanceof Blob) {
return new Promise(function (resolve, reject) {
var r = new FileReader();
var resData = response.data;
if (response.config.url.indexOf("poster/createPoster") >= 0) {
if (resData.type == "application/json") {
r.readAsText(resData);
} else {
r.readAsDataURL(resData);
}
} else {
r.readAsText(resData);
}
r.onload = function () {
let res = {};
//PK 为二进制压缩包(ZIP)导出数据流
if (
escape(r.result).indexOf("%u") == 0 ||
escape(r.result).indexOf("PK") == 0 ||
r.result.indexOf("pdf") >= 0 ||
r.result.indexOf("PDF") >= 0 ||
r.result.indexOf("data:image") >= 0
) {
res.type = "blob";
res.value = resData;
} else {
res.type = "object";
res.value = JSON.parse(r.result);
}
resolve(res);
};
}).catch((e) => {});
} else {
return {
url: response.config.url,
...response.data,
};
}
}
} else if (response.status == 302) {
catchFun("登陆失效,请重新登陆");
} else if (response.status == 401 || response.status == 403) {
catchFun("账户状态异常");
} else {
if (sessionStorage.notFirstIn) {
catchFun("网络异常,请稍后再试");
}
}
},
(error) => {
if (sessionStorage.notFirstIn) {
catchFun("网络异常,请稍后再试");
}
}
);
export default service;
const storesData = {}
export default storesData
\ No newline at end of file
......@@ -45,11 +45,11 @@
>
<!-- 手机号 -->
<div class="flex flex-col gap-2.5">
<label for="phone" class="text-white">
<label class="text-white">
手机号
</label>
<el-form-item prop="phone" class="mb-0">
<el-input
id="phone"
v-model="loginForm.phone"
type="tel"
placeholder="请输入手机号"
......@@ -58,16 +58,17 @@
:disabled="isLoading"
size="large"
/>
</el-form-item>
</div>
<!-- 图形验证码 -->
<div class="flex flex-col gap-2.5">
<label for="captcha" class="text-white">
<label class="text-white">
图形验证码
</label>
<div class="flex gap-2">
<el-form-item prop="captchaInput" class="mb-0">
<div class="flex gap-2" style="width: 100%;">
<el-input
id="captcha"
v-model="loginForm.captchaInput"
type="text"
placeholder="请输入图形验证码"
......@@ -90,16 +91,17 @@
<RefreshCw class="absolute top-1 right-1 w-3 h-3 text-gray-400" />
</div>
</div>
</el-form-item>
</div>
<!-- 短信验证码 -->
<div class="flex flex-col gap-2.5">
<label for="smsCode" class="text-white">
<label class="text-white">
短信验证码
</label>
<div class="flex gap-2">
<el-form-item prop="smsCode" class="mb-0">
<div class="flex gap-2" style="width: 100%;">
<el-input
id="smsCode"
v-model="loginForm.smsCode"
type="text"
placeholder="请输入短信验证码"
......@@ -117,8 +119,10 @@
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</div>
<div class="mt-2">
<el-button
type="primary"
@click="handleSubmit"
......@@ -128,6 +132,7 @@
>
{{ isLoading ? '登录中...' : '登录' }}
</el-button>
</div>
</el-form>
</div>
</div>
......@@ -145,6 +150,15 @@
import { ref, onMounted, onUnmounted, reactive } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { RefreshCw } from 'lucide-vue-next'
import { getCurrentInstance } from 'vue'
// 扩展组件实例类型以包含全局属性
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$api: any
$utils: any
}
}
// 导入图片资源
import loginBackgroundImg from '@/assets/7f0599d246217c734650d105801453a4919de13c.png'
......@@ -167,17 +181,20 @@ interface LoginProps {
// Props
const props = defineProps<LoginProps>()
// 获取全局API实例
const { $api } = getCurrentInstance()!.appContext.config.globalProperties
// 响应式数据
const loginFormRef = ref<FormInstance>()
const isLoading = ref(false)
const captchaText = ref('')
const captchaToken = ref('')
const captchaImage = ref('')
const countdown = ref(0)
const canSendSms = ref(true)
// 表单数据
const loginForm = reactive<LoginForm>({
phone: '13800000001',
phone: '13112345678',
captchaInput: '',
smsCode: ''
})
......@@ -198,51 +215,19 @@ const loginRules: FormRules<LoginForm> = {
]
}
// 生成图形验证码 - 完全复制React版本的逻辑
const generateCaptcha = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let text = ''
for (let i = 0; i < 4; i++) {
text += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaText.value = text
// 生成验证码图片 (使用 canvas)
const canvas = document.createElement('canvas')
canvas.width = 120
canvas.height = 40
const ctx = canvas.getContext('2d')
if (ctx) {
// 背景
ctx.fillStyle = '#f0f0f0'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 干扰线
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = `rgba(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255},0.3)`
ctx.beginPath()
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height)
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height)
ctx.stroke()
}
// 验证码文字
ctx.font = 'bold 24px Arial'
ctx.textBaseline = 'middle'
for (let i = 0; i < text.length; i++) {
ctx.fillStyle = `rgb(${Math.random() * 100},${Math.random() * 100},${Math.random() * 100})`
const x = 20 + i * 25
const y = 20 + (Math.random() - 0.5) * 8
const angle = (Math.random() - 0.5) * 0.4
ctx.save()
ctx.translate(x, y)
ctx.rotate(angle)
ctx.fillText(text[i], 0, 0)
ctx.restore()
// 从接口获取图形验证码
const generateCaptcha = async () => {
try {
const response = await $api.getImgCode({})
if (response && response.c === 0) {
captchaImage.value = 'data:image/png;base64,'+response.d.image
// 保存验证码标识,用于后续验证
captchaToken.value = response.d.imageId
} else {
ElMessage.error('获取图形验证码失败')
}
captchaImage.value = canvas.toDataURL()
} catch (error) {
ElMessage.error('获取图形验证码失败')
}
}
......@@ -266,7 +251,7 @@ const startCountdown = () => {
}
// 发送短信验证码
const handleSendSms = () => {
const handleSendSms = async () => {
// 验证手机号
if (!loginForm.phone.trim()) {
ElMessage.error('请输入手机号')
......@@ -285,34 +270,55 @@ const handleSendSms = () => {
return
}
if (loginForm.captchaInput.toUpperCase() !== captchaText.value) {
ElMessage.error('图形验证码错误')
generateCaptcha()
loginForm.captchaInput = ''
// 模拟验证图形验证码(实际应该调用接口验证)
if (!captchaToken.value) {
ElMessage.error('请先获取图形验证码')
return
}
// 模拟发送短信
try {
// 调用获取短信验证码接口
const response = await $api.getTelCode({
phone: loginForm.phone,
code: loginForm.captchaInput,
imageId: captchaToken.value
})
if (response && response.c === 0) {
ElMessage.success('验证码已发送至您的手机,请注意查收')
startCountdown()
// 演示用:实际验证码为 123456
console.log('演示验证码:123456')
} else {
ElMessage.error(response?.msg || '发送验证码失败')
generateCaptcha()
loginForm.captchaInput = ''
}
} catch (error) {
console.error('发送短信验证码失败:', error)
ElMessage.error('发送验证码失败')
generateCaptcha()
loginForm.captchaInput = ''
}
}
// 登录提交
const handleSubmit = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
} catch {
// 表单校验
const valid = await loginFormRef.value.validate()
console.log('表单验证结果:', valid)
if (!valid) {
console.log('表单验证失败')
ElMessage.error('请检查表单填写是否正确')
return
}
// 验证图形验证码
if (loginForm.captchaInput.toUpperCase() !== captchaText.value) {
ElMessage.error('图形验证码错误')
// 验证图形验证码(实际应该调用接口验证)
if (!captchaToken.value) {
ElMessage.error('请先获取图形验证码')
generateCaptcha()
loginForm.captchaInput = ''
return
......@@ -320,24 +326,37 @@ const handleSubmit = async () => {
isLoading.value = true
// 模拟登录验证 - 完全复制React版本的逻辑
setTimeout(() => {
// 演示账号:
// 13800000001 验证码123456 - 管理员
// 13800000002 验证码123456 - 普通用户
if (loginForm.phone === '13800000001' && loginForm.smsCode === '123456') {
try {
// 调用手机登录接口
const response = await $api.pohoneLogin({
phone: loginForm.phone,
code: loginForm.smsCode,
// captcha: loginForm.captchaInput,
// captchaToken: captchaToken.value
})
if (response && response.c === 0) {
ElMessage.success('登录成功')
// 保存登录信息
if (response.d) {
localStorage.setItem('pcUserInfo', JSON.stringify(response.d))
}
props.onLogin(loginForm.phone, 'admin')
} else if (loginForm.phone === '13800000002' && loginForm.smsCode === '123456') {
ElMessage.success('登录成功')
props.onLogin(loginForm.phone, 'viewer')
} else {
ElMessage.error('手机号或验证码错误')
isLoading.value = false
ElMessage.error(response?.msg || '登录失败')
generateCaptcha()
loginForm.captchaInput = ''
loginForm.smsCode = ''
}
} catch (error) {
console.error('登录失败:', error)
ElMessage.error('登录失败')
generateCaptcha()
loginForm.captchaInput = ''
loginForm.smsCode = ''
} finally {
isLoading.value = false
}
}, 800)
}
// 生命周期
......@@ -440,4 +459,9 @@ label {
font-weight: 500;
line-height: 1.5;
}
/* 重置 el-form-item 的默认边距 */
:deep(.el-form-item) {
margin-bottom: 0;
}
</style>
\ No newline at end of file
......@@ -20,6 +20,5 @@
interface Props {
className?: string
}
defineProps<Props>()
</script>
......@@ -12,6 +12,12 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
// 引入 API 接口和工具方法
import api from './assets/js/api/interface/index.js'
import request from './assets/js/api/request.js'
import stores from './assets/js/stores/index.js'
import commonUtils from './assets/js/const/common.js'
const app = createApp(App)
// 注册Element Plus图标
......@@ -23,4 +29,10 @@ app.use(createPinia())
app.use(router)
app.use(ElementPlus)
// 将 API 接口、请求实例、状态管理和工具方法挂载到全局
app.config.globalProperties.$api = api
app.config.globalProperties.$request = request
app.config.globalProperties.$stores = stores
app.config.globalProperties.$utils = commonUtils
app.mount('#app')
......@@ -20,10 +20,10 @@ export default defineConfig({
open: true,
proxy: {
// API 请求代理配置
'/crm': {
'/hallserver': {
target: 'http://thall.51xinpai.cn/', // 后端服务地址,根据你的实际情况修改
changeOrigin: true,
rewrite: (path) => path.replace(/^\/crm/, '/crm')
rewrite: (path) => path.replace(/^\/hallserver/, '/hallserver')
},
}
},
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!