Commit 5f91557b by 李宁

1Merge branch 'channelBusi'

2 parents f585baf0 637c146e
Showing 60 changed files with 1303 additions and 604 deletions
{
"permissions": {
"allow": [
"Bash(grep -E \"\\.(js|vue|html)$\")",
"Bash(npm install axios)",
"Bash(tree src/)",
"Bash(npm run build)",
"Bash(npm run build-only)",
"Bash(npm run type-check)"
],
"deny": [],
"ask": []
}
}
\ No newline at end of file
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
{
"hash": "655e7fe4",
"configHash": "92cf418e",
"lockfileHash": "d600e0a3",
"browserHash": "d2e0d31b",
"optimized": {
"vue": {
"src": "../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "b7821ec5",
"needsInterop": false
},
"pinia": {
"src": "../../node_modules/pinia/dist/pinia.mjs",
"file": "pinia.js",
"fileHash": "a659a454",
"needsInterop": false
},
"element-plus": {
"src": "../../node_modules/element-plus/es/index.mjs",
"file": "element-plus.js",
"fileHash": "13083701",
"needsInterop": false
},
"@element-plus/icons-vue": {
"src": "../../node_modules/@element-plus/icons-vue/dist/index.js",
"file": "@element-plus_icons-vue.js",
"fileHash": "d67fc749",
"needsInterop": false
},
"axios": {
"src": "../../node_modules/axios/index.js",
"file": "axios.js",
"fileHash": "e22e391f",
"needsInterop": false
},
"vue-router": {
"src": "../../node_modules/vue-router/dist/vue-router.mjs",
"file": "vue-router.js",
"fileHash": "30652b08",
"needsInterop": false
}
},
"chunks": {
"chunk-XAE367SZ": {
"file": "chunk-XAE367SZ.js"
},
"chunk-YHMWYXEE": {
"file": "chunk-YHMWYXEE.js"
},
"chunk-G3PMV62Z": {
"file": "chunk-G3PMV62Z.js"
}
}
}
\ No newline at end of file
This diff could not be displayed because it is too large.
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
export {
__commonJS,
__export,
__toESM
};
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-YHMWYXEE.js";
import "./chunk-G3PMV62Z.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
......@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>商机办结登记</title>
</head>
<body>
<div id="app"></div>
......
......@@ -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",
......
......@@ -4,7 +4,7 @@ import LoginPage from './components/LoginPage.vue'
import DesktopMain from './components/DesktopMain.vue'
// 登录状态管理
const isLoggedIn = ref(false)
const isLoggedIn = ref(localStorage.getItem('pcUserInfo')?true:false)
const currentUser = ref<{ username: string; role: 'admin' | 'viewer' } | null>(null)
// 登录处理
......@@ -13,8 +13,17 @@ const handleLogin = (username: string, role: 'admin' | 'viewer') => {
isLoggedIn.value = true
}
if(isLoggedIn){
const userInfo = JSON.parse(localStorage.getItem('pcUserInfo') || '{}')
const name = userInfo?.nickname || ''
if(name) {
handleLogin(name, 'admin')
}
}
// 登出处理
const handleLogout = () => {
localStorage.removeItem('pcUserInfo')
currentUser.value = null
isLoggedIn.value = false
}
......
import request from '../../request'
/**
* 查询账号列表
*/
export function queryAccountList(data) {
return request({
url: '/crm/getUserPageList',
data,
})
}
/**
* 添加/更新账号
*/
export function addAndUpdateRole(data) {
return request({
url: '/crm' + (data.id?'/updateUser':'/createUser'),
data,
})
}
/**
* 获取指定区域下的所有区域
*/
export function queryAreaData(data) {
return request({
url: '/crm/getArea?areaId='+data.areaId,
method: 'GET'
})
}
\ 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 queryOrderList(data) {
return request({
url: '/crm/getOrderList',
data,
})
}
/**
* 订单列表导出
* @returns {AxiosPromise}
*/
export function exportOrderList(data) {
return request({
url: '/crm/exportOrders',
data,
responseType: 'blob'
})
}
/**
* 订单审核
* @returns {AxiosPromise}
*/
export function audioOrderList(data) {
return request({
url: '/crm/auditOrder',
data,
})
}
/**
* 订单金额批量修改:表格上传
* @returns {AxiosPromise}
*/
export function updateOrderListMoney(data) {
return request({
url: '/crm/readExcelModifyOrder',
headers: {
'Content-Type': 'multipart/form-data'
},
data,
})
}
/**
* 订单金额修改
* @returns {AxiosPromise}
*/
export function updateOrderMoney(data) {
return request({
url: '/crm/updateOrderMoney',
data,
})
}
/**
* 订单crm修改
* @returns {AxiosPromise}
*/
export function updateOrderCrm(data) {
return request({
url: '/crm/updateCrmOrderId',
data,
})
}
/**
* 订单关闭
* @returns {AxiosPromise}
*/
export function closeOrder(data) {
return request({
url: '/crm/closeOrder',
data,
})
}
/**
* 订单日志
* @returns {AxiosPromise}
*/
export function queryOrderLog(data) {
return request({
url: '/crm/getOrderLog',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 业务酬金列表查询
*/
export function queryAllRewardList() {
return request({
url: '/crm/getJobsList',
method: 'GET'
})
}
/**
* 业务酬金批量上传:表格读取
*/
export function uploadReward(formData) {
return request({
url: '/crm/readExcelJobs',
headers: {
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
/**
* 业务酬金批量上传
*/
export function uploadRewardSave(data) {
return request({
url: '/crm/batchUploadJobs',
data,
})
}
/**
* 业务酬金新增和修改
*/
export function updateReward(data) {
return request({
url: data.id?'/crm/updateJob':'/crm/addJobs',
data,
})
}
/**
* 业务酬金删除
*/
export function delReward(data) {
return request({
url: '/crm/delJob',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 查询角色列表
*/
export function queryRoleList(data) {
return request({
url: '/crm/getRoleList',
method: 'GET',
data,
})
}
/**
* 创建和修改角色
*/
export function createOrUpdateRole(data) {
let url = '/crm/createRole'
if(data.roleId){
url = '/crm/updateRole'
}
return request({
url,
data,
})
}
/**
* 根据角色ID获取角色权限
*/
export function queryRolePermission(data) {
return request({
url: '/crm/getRoleFunction',
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["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="请输入图形验证码"
......@@ -87,19 +88,19 @@
alt="验证码"
class="w-full h-full object-cover"
/>
<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 +118,10 @@
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</div>
<div class="mt-2">
<el-button
type="primary"
@click="handleSubmit"
......@@ -128,6 +131,7 @@
>
{{ isLoading ? '登录中...' : '登录' }}
</el-button>
</div>
</el-form>
</div>
</div>
......@@ -144,7 +148,7 @@
<script setup lang="ts">
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'
// 导入图片资源
import loginBackgroundImg from '@/assets/7f0599d246217c734650d105801453a4919de13c.png'
......@@ -167,19 +171,22 @@ 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: ''
smsCode: '123456'
})
// 表单验证规则
......@@ -198,56 +205,24 @@ 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('获取图形验证码失败')
}
}
// 倒计时逻辑
let countdownTimer: NodeJS.Timeout | null = null
let countdownTimer: number | null = null
const startCountdown = () => {
countdown.value = 60
......@@ -266,7 +241,7 @@ const startCountdown = () => {
}
// 发送短信验证码
const handleSendSms = () => {
const handleSendSms = async () => {
// 验证手机号
if (!loginForm.phone.trim()) {
ElMessage.error('请输入手机号')
......@@ -285,34 +260,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?.m || '发送验证码失败')
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 +316,37 @@ const handleSubmit = async () => {
isLoading.value = true
// 模拟登录验证 - 完全复制React版本的逻辑
setTimeout(() => {
// 演示账号:
// 13800000001 验证码123456 - 管理员
// 13800000002 验证码123456 - 普通用户
if (loginForm.phone === '13800000001' && loginForm.smsCode === '123456') {
ElMessage.success('登录成功')
props.onLogin(loginForm.phone, 'admin')
} else if (loginForm.phone === '13800000002' && 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('登录成功')
props.onLogin(loginForm.phone, 'viewer')
// 保存登录信息
if (response.d) {
localStorage.setItem('pcUserInfo', JSON.stringify(response.d))
}
props.onLogin(response.d.nickname, 'admin')
} else {
ElMessage.error('手机号或验证码错误')
isLoading.value = false
ElMessage.error(response?.m || '登录失败')
generateCaptcha()
loginForm.captchaInput = ''
loginForm.smsCode = ''
}
} catch (error) {
console.error('登录失败:', error)
ElMessage.error('登录失败')
generateCaptcha()
loginForm.captchaInput = ''
loginForm.smsCode = ''
} finally {
isLoading.value = false
}
}, 800)
}
// 生命周期
......@@ -440,4 +449,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
......@@ -48,7 +48,6 @@
class="inline-block w-2 h-2 rounded-sm bg-white"
/>
</div>
<!-- 组织图标 -->
<Building2
:class="[
......@@ -110,11 +109,9 @@
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Building2, ChevronDown, ChevronRight } from 'lucide-vue-next'
interface Organization {
id: string
name: string
......@@ -122,35 +119,30 @@ interface Organization {
parentId?: string
children?: Organization[]
}
interface Role {
id: string
name: string
level: '地市级' | '区县级' | '一线人员'
}
interface Props {
organizations: Organization[]
selectedId?: string
roleId?: string
roles?: Role[]
expandedIds?: Set<string>
accountType?: '地市级' | '区县级' | '一线人员'
accountType?: '1' | '2' | '3'
}
const props = withDefaults(defineProps<Props>(), {
selectedId: '',
roleId: '',
roles: () => [],
expandedIds: () => new Set(),
accountType: ''
accountType: undefined
})
const emit = defineEmits<{
select: [orgId: string]
'toggle-expand': [orgId: string]
}>()
// 如果没有传入 expandedIds,使用本地状态
const localExpandedIds = ref(new Set<string>())
const expandedIds = computed(() =>
......@@ -158,36 +150,28 @@ const expandedIds = computed(() =>
? props.expandedIds
: localExpandedIds.value
)
// 工具函数
const hasChildren = (org: Organization): boolean => {
return !!(org.children && org.children.length > 0)
}
const isSelectable = (org: Organization): boolean => {
console.log('检查组织可选性:', org.name, org.type, '账号类型:', props.accountType)
//console.log('检查组织可选性:', org.name, org.type, '账号类型:', props.accountType)
// 只根据账号类型判断,不根据角色层级判断
if (props.accountType) {
switch (props.accountType) {
case '地市级':
case '1':
// 地市级只能选择地市级组织
console.log('地市级账号,检查组织类型是否为地市:', org.type === '地市')
//console.log('地市级账号,检查组织类型是否为地市:', org.type === '地市')
return org.type === '地市'
case '区县级':
case '2':
// 区县级只能选择区县级组织
console.log('区县级账号,检查组织类型是否为区县:', org.type === '区县')
//console.log('区县级账号,检查组织类型是否为区县:', org.type === '区县')
return org.type === '区县'
case '3':
// 区县级只能选择区县级组织
//console.log('区县级账号,检查组织类型是否为区县:', org.type === '区县')
return org.type === '区县'
case '一线人员':
// 一线人员只能选择区县下的客户经理团队
console.log('一线人员账号,检查组织类型是否为客户经理团队:', org.type === '客户经理团队')
if (org.type !== '客户经理团队') {
return false
}
// 检查该客户经理团队是否属于某个区县
const result = isUnderCounty(org)
console.log('客户经理团队是否属于区县下:', result)
return result
default:
return true
}
......@@ -196,7 +180,6 @@ const isSelectable = (org: Organization): boolean => {
// 如果没有账号类型,可以选择所有组织
return true
}
// 检查组织是否属于区县下
const isUnderCounty = (org: Organization): boolean => {
console.log('OrganizationTree.vue:202 检查组织是否属于区县下:', org.name, org.type, org.id)
......@@ -239,7 +222,6 @@ const isUnderCounty = (org: Organization): boolean => {
console.log('OrganizationTree.vue:241 未找到区县父组织')
return false
}
const getOrgTypeLabel = (type: Organization['type']) => {
switch (type) {
case '地市':
......@@ -252,21 +234,18 @@ const getOrgTypeLabel = (type: Organization['type']) => {
return type
}
}
const getRoleLevel = (roleId: string): string => {
if (!props.roles || props.roles.length === 0) return '区县级'
const role = props.roles.find(r => r.id === roleId)
return role ? role.level : '区县级'
}
// 事件处理
const handleSelect = (org: Organization) => {
if (isSelectable(org)) {
emit('select', org.id)
}
}
const toggleExpand = (orgId: string) => {
if (props.expandedIds && props.expandedIds.size > 0) {
// 使用父组件的展开状态
......@@ -280,11 +259,9 @@ const toggleExpand = (orgId: string) => {
}
}
}
const handleToggleExpand = (orgId: string) => {
emit('toggle-expand', orgId)
}
// 初始化展开顶级组织
if (props.expandedIds && props.expandedIds.size === 0) {
props.organizations.forEach(org => {
......@@ -293,128 +270,100 @@ if (props.expandedIds && props.expandedIds.size === 0) {
}
})
}
</script>
</script>
<style scoped>
.space-y-1 > * + * {
margin-top: 0.25rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.gap-2 {
gap: 0.5rem;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
.rounded {
border-radius: 0.25rem;
}
.cursor-pointer {
cursor: pointer;
}
.cursor-not-allowed {
cursor: not-allowed;
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.opacity-40 {
opacity: 0.4;
}
.bg-blue-50 {
background-color: #eff6ff;
}
.border {
border-width: 1px;
}
.border-blue-500 {
border-color: #3b82f6;
}
.hover\:bg-neutral-50:hover {
background-color: #f9fafb;
}
.hover\:bg-neutral-200:hover {
background-color: #e5e7eb;
}
.h-4 {
height: 1rem;
}
.w-4 {
width: 1rem;
}
.w-6 {
width: 1.5rem;
}
.text-neutral-300 {
color: #d1d5db;
}
.text-neutral-400 {
color: #9ca3af;
}
.text-neutral-500 {
color: #6b7280;
}
.text-neutral-600 {
color: #4b5563;
}
.text-neutral-900 {
color: #111827;
}
.text-blue-600 {
color: #2563eb;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.flex-1 {
flex: 1 1 0%;
}
.ml-4 {
margin-left: 1rem;
}
.border-l-2 {
border-left-width: 2px;
}
.border-neutral-200 {
border-color: #e5e7eb;
}
......
<template>
<div class="space-y-1">
<div
:class="`permission-tree-node flex items-center gap-2 p-2 rounded ${level > 0 ? 'ml-6' : ''}`"
>
:class="`permission-tree-node flex items-center gap-2 p-2 rounded ${level > 0 ? 'ml-6' : ''}`">
<!-- 展开/收起按钮 -->
<button
v-if="hasChildren"
......@@ -27,18 +26,18 @@
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-neutral-900">{{ permission.name }}</span>
<el-tag
<!-- <el-tag
type="info"
effect="plain"
size="small"
class="text-xs"
>
{{ permission.code }}
</el-tag>
</el-tag> -->
</div>
<p v-if="permission.description" class="text-xs text-neutral-500 mt-1">
<!-- <p v-if="permission.description" class="text-xs text-neutral-500 mt-1">
{{ permission.description }}
</p>
</p> -->
</div>
</div>
......@@ -103,9 +102,29 @@ const isSelected = computed(() =>
const isIndeterminate = computed(() => {
if (isSelected.value || !hasChildren.value) return false
return props.permission.children!.some(c =>
props.selectedPermissions.includes(c.id)
)
// 获取所有子权限ID
const getAllChildIds = (children: Permission[]): string[] => {
const ids: string[] = []
for (const child of children) {
ids.push(child.id)
if (child.children) {
ids.push(...getAllChildIds(child.children))
}
}
return ids
}
const allChildIds = getAllChildIds(props.permission.children!)
if (allChildIds.length === 0) return false
// 检查是否所有子权限都被选中
const allSelected = allChildIds.every(id => props.selectedPermissions.includes(id))
if (allSelected) return false
// 检查是否有部分子权限被选中
const someSelected = allChildIds.some(id => props.selectedPermissions.includes(id))
return someSelected
})
</script>
......
......@@ -23,44 +23,28 @@
:data="filteredRoles"
style="width: 100%"
:header-cell-style="{ backgroundColor: '#f3f4f6', color: '#374151', fontWeight: '500', borderBottom: '1px solid #e5e7eb' }"
:row-style="{ borderBottom: '1px solid rgb(243 244 246)' }"
>
<el-table-column prop="name" label="角色名称" min-width="120">
<template #default="{ row }">
<span class="text-neutral-900">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="permissionIds" label="权限数量" min-width="100">
:row-style="{ borderBottom: '1px solid rgb(243 244 246)' }">
<el-table-column prop="roleName" label="角色名称" min-width="120">
<template #default="{ row }">
<el-tag
type="info"
effect="plain"
size="small"
class="bg-neutral-50"
>
{{ row.permissionIds.length }} 个权限
</el-tag>
<span class="text-neutral-900">{{ row.roleName }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template #default="{ row }">
<el-tag
:type="row.status === '启用' ? 'success' : 'info'"
:type="row.status == 1 ? 'success' : 'info'"
effect="plain"
size="small"
>
{{ row.status }}
{{ row.status==1?'启用':'停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="150">
<template #default="{ row }">
<span class="text-neutral-600">{{ row.createTime || '-' }}</span>
<span class="text-neutral-600">{{ $utils.detailTime(row.createTime)}}</span>
</template>
</el-table-column>
......@@ -132,6 +116,8 @@
</div>
<el-switch
v-model="roleStatusEnabled"
active-text="开启"
inactive-text="关闭"
/>
</div>
</div>
......@@ -142,13 +128,13 @@
<label class="block text-sm font-medium text-neutral-900">
权限选择 <span class="text-red-500">*</span>
</label>
<el-tag
<!-- <el-tag
type="primary"
effect="plain"
class="bg-brand-primary/10 text-brand-primary border-brand-primary"
>
已选择 {{ selectedPermissions.length }} 个权限
</el-tag>
</el-tag> -->
</div>
<div class="border border-neutral-300 rounded p-4 bg-neutral-50 max-h-96 overflow-y-auto">
......@@ -191,11 +177,32 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref,onMounted, computed, watch ,getCurrentInstance} from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import PermissionTreeNode from './PermissionTreeNode.vue'
const { $api,$utils } = getCurrentInstance()!.appContext.config.globalProperties
onMounted(() => {
// 初始化数据
handleFilter()
})
const handleFilter = async () => {
try {
const response = await $api.queryRoleList({});
if (response.c === 0) {
filteredRoles.value = response.d
ElMessage.success('查询成功');
} else {
ElMessage.error(response.msg || '查询失败');
}
} catch (error: any) {
ElMessage.error('查询失败: ' + error.message);
}
}
// 类型定义
export interface Permission {
id: string
......@@ -207,17 +214,18 @@ export interface Permission {
}
export interface Role {
id: string
id: Number
name: string
description?: string
level: RoleLevel
permissionIds: string[]
roleName?: string
level: '地市级' | '区县级' | '一线人员'
status: RoleStatus
remark?: string
permissionIds?: string[]
permissions?: string[]
createTime?: string
}
export type RoleLevel = '地市级' | '区县级' | '一线人员'
export type RoleStatus = '启用' | '禁用'
export type RoleStatus = 1 | 0
// Props
interface Props {
......@@ -242,50 +250,101 @@ const roleName = ref('')
const roleDescription = ref('')
const selectedPermissions = ref<string[]>([])
const roleLevel = ref<'地市级' | '区县级' | '一线人员'>('区县级')
const roleStatusEnabled = ref(true)
// 权限树展开状态
const expandedPermissions = ref<Set<string>>(new Set())
// 计算属性
const roleStatus = computed((): RoleStatus => roleStatusEnabled.value ? '启用' : '禁用')
const roleStatus = computed((): RoleStatus => roleStatusEnabled.value ? 1 : 0)
const topLevelPermissions = computed(() =>
props.permissions.filter(p => !p.parentId)
)
const filteredRoles = computed(() =>
props.roles.filter(role => role.name !== '地市主管理员')
)
const filteredRoles = ref([])
const getAllIds = (tree: any[]) => {
const ids = [] // 结果池
const stack = [...tree] // 根节点入栈
while (stack.length) {
const node = stack.pop()
if (node.id !== undefined) ids.push(node.id) // 收集当前节点
if (node.children?.length) {
// 子节点全部入栈(顺序无所谓就 push,要顺序就 unshift)
stack.push(...node.children)
}
}
return ids
}
// 获取角色的权限ID列表,兼容不同的字段名
const getRolePermissionIds = async (role: Role)=> {
try {
const response = await $api.queryRolePermission({
id: role.id
})
if (response.c === 0 && response.d) {
return getAllIds(response.d.list)
} else {
console.warn('获取角色权限失败:', response.msg)
return role.permissionIds || role.permissions || []
}
} catch (error) {
console.error('获取角色权限出错:', error)
// 如果API调用失败,使用角色对象中已有的权限数据
return role.permissionIds || role.permissions || []
}
}
// 打开新增对话框
const handleOpenAddDialog = () => {
editingRole.value = null
roleName.value = ''
roleDescription.value = ''
roleStatusEnabled.value = true
selectedPermissions.value = []
roleStatusEnabled.value = true
expandedPermissions.value = new Set(props.permissions.filter(p => !p.parentId).map(p => p.id))
isDialogOpen.value = true
}
// 打开编辑对话框
const handleOpenEditDialog = (role: Role) => {
const handleOpenEditDialog = async (role: Role) => {
editingRole.value = role
roleName.value = role.name
roleDescription.value = role.description || ''
roleName.value = role.roleName || ''
roleDescription.value = role.remark || ''
roleStatusEnabled.value = role.status === 1
selectedPermissions.value = [...role.permissionIds]
roleStatusEnabled.value = role.status === '启用'
// 获取角色权限并设置选中状态
const permissionIds = await getRolePermissionIds(role)
selectedPermissions.value = permissionIds
// 展开顶级权限以便用户能看到选中的权限
expandedPermissions.value = new Set(props.permissions.filter(p => !p.parentId).map(p => p.id))
// 如果有选中的权限,也展开其父级权限
permissionIds.forEach(permissionId => {
const permission = findPermissionById(permissionId)
if (permission && permission.parentId) {
// 向上展开所有父级
let currentPermission = permission
while (currentPermission.parentId) {
expandedPermissions.value.add(currentPermission.parentId)
currentPermission = findPermissionById(currentPermission.parentId)!
}
}
})
isDialogOpen.value = true
}
// 保存角色
const handleSave = () => {
const handleSave = async () => {
if (!roleName.value.trim()) {
ElMessage.error('请输入角色名称')
return
......@@ -296,25 +355,41 @@ const handleSave = () => {
return
}
const roleData = {
name: roleName.value.trim(),
description: roleDescription.value.trim(),
permissionIds: selectedPermissions.value,
status: roleStatus.value
}
if (editingRole.value) {
emit('updateRole', editingRole.value.id, roleData)
ElMessage.success('角色更新成功')
const response = await $api.createOrUpdateRole({
roleId: editingRole.value?editingRole.value.id:'',
roleName: roleName.value.trim(),
remark: roleDescription.value.trim(),
status: roleStatus.value,
functionIds: selectedPermissions.value
})
if (response.c === 0) {
handleFilter()
ElMessage.success(editingRole.value?'编辑成功':'创建成功');
} else {
emit('addRole', roleData)
ElMessage.success('角色创建成功')
ElMessage.error(response.m);
}
isDialogOpen.value = false
}
// 获取所有子权限ID
const getAllChildPermissionIds = (permissionId: string): string[] => {
const permission = findPermissionById(permissionId)
if (!permission || !permission.children) return []
const childIds: string[] = []
const collectChildIds = (perms: Permission[]) => {
for (const p of perms) {
childIds.push(p.id)
if (p.children) {
collectChildIds(p.children)
}
}
}
collectChildIds(permission.children)
return childIds
}
// 切换权限选择
const handleTogglePermission = (permissionId: string) => {
const permission = findPermissionById(permissionId)
......@@ -330,8 +405,18 @@ const handleTogglePermission = (permissionId: string) => {
})
newSelected = newSelected.filter(id => id !== permissionId)
} else {
// 选择:自动选择所有父权限
// 选择:自动选择所有父权限和所有子权限
newSelected.push(permissionId)
// 自动选择所有子权限
const childIds = getAllChildPermissionIds(permissionId)
childIds.forEach(id => {
if (!newSelected.includes(id)) {
newSelected.push(id)
}
})
// 自动选择所有父权限
let current = permission
while (current.parentId) {
if (!newSelected.includes(current.parentId)) {
......@@ -342,6 +427,8 @@ const handleTogglePermission = (permissionId: string) => {
}
selectedPermissions.value = newSelected
console.log(newSelected)
}
// 查找权限
......
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
// import WelcomeItem from './WelcomeItem.vue' // 文件不存在,暂时注释
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
......
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
<template>
<div :class="className || 'relative size-full'">
<svg class="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 1024 1024">
<g>
<!-- 角色管理图标 - 用户+加号 -->
<path
d="M471 921.6H225.3c-22.6 0-41-18.4-41-41V757.8c0-112.9 91.9-204.8 204.8-204.8h245.8c43 0 84.3 13.4 119.4 38.7 18.4 13.3 43.9 9.1 57.2-9.2 13.3-18.3 9.1-43.9-9.2-57.2-49.1-35.5-107-54.2-167.4-54.2H389.1C231 471 102.4 599.7 102.4 757.8v122.9c0 67.8 55.1 122.9 122.9 122.9H471c22.6 0 41-18.3 41-41s-18.3-41-41-41zM512 430.1c113.1 0 204.8-91.7 204.8-204.8S625.1 20.5 512 20.5s-204.8 91.7-204.8 204.8S398.9 430.1 512 430.1z m0-327.7c67.8 0 122.9 55.1 122.9 122.9S579.8 348.2 512 348.2 389.1 293 389.1 225.3 444.2 102.4 512 102.4z"
fill="currentColor"
/>
<path
d="M880.6 798.7h-81.9v-81.9c0-22.5-18.4-41-41-41-22.5 0-41 18.4-41 41v81.9h-81.9c-22.5 0-41 18.4-41 41 0 22.5 18.4 41 41 41h81.9v81.9c0 22.5 18.4 41 41 41 22.5 0 41-18.4 41-41v-81.9h81.9c22.5 0 41-18.4 41-41 0-22.5-18.4-41-41-41z"
fill="currentColor"
/>
</g>
</svg>
</div>
</template>
<script setup lang="ts">
interface Props {
className?: string
}
defineProps<Props>()
</script>
......@@ -12,6 +12,14 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
// 引入 API 接口和工具方法
// @ts-ignore
import api from './assets/js/api/interface/index.js'
// @ts-ignore
import stores from './assets/js/stores/index.js'
// @ts-ignore
import commonUtils from './assets/js/const/common.js'
const app = createApp(App)
// 注册Element Plus图标
......@@ -23,4 +31,9 @@ app.use(createPinia())
app.use(router)
app.use(ElementPlus)
// 将 API 接口、请求实例、状态管理和工具方法挂载到全局
app.config.globalProperties.$api = api
app.config.globalProperties.$stores = stores
app.config.globalProperties.$utils = commonUtils
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
{
path: '/role-test',
name: 'role-test',
component: () => import('../views/RoleTestView.vue'),
},
],
})
......
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
<template>
<div class="min-h-screen bg-gray-50 p-6">
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold text-gray-900 mb-6">角色管理测试页面</h1>
<RoleManagement
:roles="roles"
:permissions="permissions"
@add-role="handleAddRole"
@update-role="handleUpdateRole"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RoleManagement from '@/components/RoleManagement.vue'
import { ElMessage } from 'element-plus'
// 类型定义
interface Role {
id: string
name: string
description?: string
level: '地市级' | '区县级' | '一线人员'
permissionIds: string[]
status: '启用' | '禁用'
createTime?: string
}
interface Permission {
id: string
name: string
code: string
description?: string
parentId?: string
children?: Permission[]
}
// 测试数据
const roles = ref<Role[]>([
{
id: 'role-001',
name: '区县全权管理员',
description: '拥有区县级别的所有管理权限,可以管理订单、用户和业务规则',
level: '区县级',
permissionIds: [
'order',
'order:view',
'order:complete',
'order:reward',
'order:approve',
'business',
'business:view',
'business:create',
'business:edit',
'system',
'system:role',
'system:role:view',
'system:role:create',
'system:role:edit',
'system:account',
'system:account:view',
'system:account:create',
'system:account:edit'
],
status: '启用',
createTime: '2025-10-20 09:00:00'
},
{
id: 'role-002',
name: '订单管理员',
description: '负责订单的日常管理和处理',
level: '一线人员',
permissionIds: ['order', 'order:view', 'order:complete', 'order:reward', 'order:approve'],
status: '启用',
createTime: '2025-10-20 09:30:00'
},
{
id: 'role-003',
name: '业务规则管理员',
description: '负责业务规则的配置和维护',
level: '区县级',
permissionIds: ['business', 'business:view', 'business:create', 'business:edit'],
status: '启用',
createTime: '2025-10-20 10:00:00'
}
])
const permissions = ref<Permission[]>([
{
id: 'order',
name: '订单管理',
code: 'order',
description: '订单相关的所有权限',
children: [
{
id: 'order:view',
name: '查看订单',
code: 'order:view',
description: '查看订单列表和详情',
parentId: 'order'
},
{
id: 'order:complete',
name: '填写办结信息',
code: 'order:complete',
description: '填写CRM订单编号和办理备注',
parentId: 'order'
},
{
id: 'order:reward',
name: '填写酬金金额',
code: 'order:reward',
description: '填写和修改实际发放酬金',
parentId: 'order'
},
{
id: 'order:approve',
name: '审核',
code: 'order:approve',
description: '审核通过或驳回订单',
parentId: 'order'
}
]
},
{
id: 'business',
name: '业务规则管理',
code: 'business',
description: '业务规则相关权限',
children: [
{
id: 'business:view',
name: '查看业务规则',
code: 'business:view',
description: '查看业务规则列表',
parentId: 'business'
},
{
id: 'business:create',
name: '创建业务规则',
code: 'business:create',
description: '创建新的业务规则',
parentId: 'business'
},
{
id: 'business:edit',
name: '编辑业务规则',
code: 'business:edit',
description: '修改和停用业务规则',
parentId: 'business'
}
]
},
{
id: 'system',
name: '系统管理',
code: 'system',
description: '系统管理相关权限',
children: [
{
id: 'system:role',
name: '角色管理',
code: 'system:role',
description: '角色管理相关权限',
parentId: 'system',
children: [
{
id: 'system:role:view',
name: '查看角色',
code: 'system:role:view',
description: '查看角色列表',
parentId: 'system:role'
},
{
id: 'system:role:create',
name: '创建角色',
code: 'system:role:create',
description: '创建新角色',
parentId: 'system:role'
},
{
id: 'system:role:edit',
name: '编辑角色',
code: 'system:role:edit',
description: '编辑角色信息和权限',
parentId: 'system:role'
}
]
},
{
id: 'system:account',
name: '账号管理',
code: 'system:account',
description: '账号管理相关权限',
parentId: 'system',
children: [
{
id: 'system:account:view',
name: '查看账号',
code: 'system:account:view',
description: '查看账号列表',
parentId: 'system:account'
},
{
id: 'system:account:create',
name: '创建账号',
code: 'system:account:create',
description: '创建新账号',
parentId: 'system:account'
},
{
id: 'system:account:edit',
name: '编辑账号',
code: 'system:account:edit',
description: '编辑账号信息',
parentId: 'system:account'
}
]
}
]
}
])
// 事件处理
const handleAddRole = (roleData: Omit<Role, 'id' | 'createTime'>) => {
const newRole: Role = {
id: `role-${Date.now()}`,
...roleData,
createTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
roles.value.push(newRole)
ElMessage.success('角色创建成功')
console.log('新增角色:', newRole)
}
const handleUpdateRole = (roleId: string, updates: Partial<Role>) => {
const roleIndex = roles.value.findIndex(role => role.id === roleId)
if (roleIndex !== -1) {
roles.value[roleIndex] = { ...roles.value[roleIndex], ...updates }
ElMessage.success('角色更新成功')
console.log('更新角色:', roleId, updates)
}
}
</script>
<style scoped>
/* 测试页面样式 */
</style>
......@@ -10,9 +10,22 @@ export default defineConfig({
vue(),
// vueDevTools(),
],
base: './',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
port: 3000,
open: true,
proxy: {
// API 请求代理配置
'/hallserver': {
target: 'http://thall.51xinpai.cn/', // 后端服务地址,根据你的实际情况修改
changeOrigin: true,
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!