Commit 5f91557b by 李宁

1Merge branch 'channelBusi'

2 parents f585baf0 637c146e
Showing 60 changed files with 1207 additions and 522 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 \ 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 \ 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 @@ ...@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>商机办结登记</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"axios": "^1.13.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"element-plus": "^2.11.7", "element-plus": "^2.11.7",
"lucide-vue-next": "^0.552.0", "lucide-vue-next": "^0.552.0",
......
...@@ -4,7 +4,7 @@ import LoginPage from './components/LoginPage.vue' ...@@ -4,7 +4,7 @@ import LoginPage from './components/LoginPage.vue'
import DesktopMain from './components/DesktopMain.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) const currentUser = ref<{ username: string; role: 'admin' | 'viewer' } | null>(null)
// 登录处理 // 登录处理
...@@ -13,8 +13,17 @@ const handleLogin = (username: string, role: 'admin' | 'viewer') => { ...@@ -13,8 +13,17 @@ const handleLogin = (username: string, role: 'admin' | 'viewer') => {
isLoggedIn.value = true isLoggedIn.value = true
} }
if(isLoggedIn){
const userInfo = JSON.parse(localStorage.getItem('pcUserInfo') || '{}')
const name = userInfo?.nickname || ''
if(name) {
handleLogin(name, 'admin')
}
}
// 登出处理 // 登出处理
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('pcUserInfo')
currentUser.value = null currentUser.value = null
isLoggedIn.value = false 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 \ 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 \ 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 \ 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 \ 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 \ 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 \ No newline at end of file
...@@ -48,7 +48,6 @@ ...@@ -48,7 +48,6 @@
class="inline-block w-2 h-2 rounded-sm bg-white" class="inline-block w-2 h-2 rounded-sm bg-white"
/> />
</div> </div>
<!-- 组织图标 --> <!-- 组织图标 -->
<Building2 <Building2
:class="[ :class="[
...@@ -110,11 +109,9 @@ ...@@ -110,11 +109,9 @@
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { Building2, ChevronDown, ChevronRight } from 'lucide-vue-next' import { Building2, ChevronDown, ChevronRight } from 'lucide-vue-next'
interface Organization { interface Organization {
id: string id: string
name: string name: string
...@@ -122,35 +119,30 @@ interface Organization { ...@@ -122,35 +119,30 @@ interface Organization {
parentId?: string parentId?: string
children?: Organization[] children?: Organization[]
} }
interface Role { interface Role {
id: string id: string
name: string name: string
level: '地市级' | '区县级' | '一线人员' level: '地市级' | '区县级' | '一线人员'
} }
interface Props { interface Props {
organizations: Organization[] organizations: Organization[]
selectedId?: string selectedId?: string
roleId?: string roleId?: string
roles?: Role[] roles?: Role[]
expandedIds?: Set<string> expandedIds?: Set<string>
accountType?: '地市级' | '区县级' | '一线人员' accountType?: '1' | '2' | '3'
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedId: '', selectedId: '',
roleId: '', roleId: '',
roles: () => [], roles: () => [],
expandedIds: () => new Set(), expandedIds: () => new Set(),
accountType: '' accountType: undefined
}) })
const emit = defineEmits<{ const emit = defineEmits<{
select: [orgId: string] select: [orgId: string]
'toggle-expand': [orgId: string] 'toggle-expand': [orgId: string]
}>() }>()
// 如果没有传入 expandedIds,使用本地状态 // 如果没有传入 expandedIds,使用本地状态
const localExpandedIds = ref(new Set<string>()) const localExpandedIds = ref(new Set<string>())
const expandedIds = computed(() => const expandedIds = computed(() =>
...@@ -158,36 +150,28 @@ const expandedIds = computed(() => ...@@ -158,36 +150,28 @@ const expandedIds = computed(() =>
? props.expandedIds ? props.expandedIds
: localExpandedIds.value : localExpandedIds.value
) )
// 工具函数 // 工具函数
const hasChildren = (org: Organization): boolean => { const hasChildren = (org: Organization): boolean => {
return !!(org.children && org.children.length > 0) return !!(org.children && org.children.length > 0)
} }
const isSelectable = (org: Organization): boolean => { const isSelectable = (org: Organization): boolean => {
console.log('检查组织可选性:', org.name, org.type, '账号类型:', props.accountType) //console.log('检查组织可选性:', org.name, org.type, '账号类型:', props.accountType)
// 只根据账号类型判断,不根据角色层级判断 // 只根据账号类型判断,不根据角色层级判断
if (props.accountType) { if (props.accountType) {
switch (props.accountType) { switch (props.accountType) {
case '地市级': case '1':
// 地市级只能选择地市级组织 // 地市级只能选择地市级组织
console.log('地市级账号,检查组织类型是否为地市:', org.type === '地市') //console.log('地市级账号,检查组织类型是否为地市:', org.type === '地市')
return 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 === '区县' return org.type === '区县'
case '一线人员':
// 一线人员只能选择区县下的客户经理团队
console.log('一线人员账号,检查组织类型是否为客户经理团队:', org.type === '客户经理团队')
if (org.type !== '客户经理团队') {
return false
}
// 检查该客户经理团队是否属于某个区县
const result = isUnderCounty(org)
console.log('客户经理团队是否属于区县下:', result)
return result
default: default:
return true return true
} }
...@@ -196,7 +180,6 @@ const isSelectable = (org: Organization): boolean => { ...@@ -196,7 +180,6 @@ const isSelectable = (org: Organization): boolean => {
// 如果没有账号类型,可以选择所有组织 // 如果没有账号类型,可以选择所有组织
return true return true
} }
// 检查组织是否属于区县下 // 检查组织是否属于区县下
const isUnderCounty = (org: Organization): boolean => { const isUnderCounty = (org: Organization): boolean => {
console.log('OrganizationTree.vue:202 检查组织是否属于区县下:', org.name, org.type, org.id) console.log('OrganizationTree.vue:202 检查组织是否属于区县下:', org.name, org.type, org.id)
...@@ -239,7 +222,6 @@ const isUnderCounty = (org: Organization): boolean => { ...@@ -239,7 +222,6 @@ const isUnderCounty = (org: Organization): boolean => {
console.log('OrganizationTree.vue:241 未找到区县父组织') console.log('OrganizationTree.vue:241 未找到区县父组织')
return false return false
} }
const getOrgTypeLabel = (type: Organization['type']) => { const getOrgTypeLabel = (type: Organization['type']) => {
switch (type) { switch (type) {
case '地市': case '地市':
...@@ -252,21 +234,18 @@ const getOrgTypeLabel = (type: Organization['type']) => { ...@@ -252,21 +234,18 @@ const getOrgTypeLabel = (type: Organization['type']) => {
return type return type
} }
} }
const getRoleLevel = (roleId: string): string => { const getRoleLevel = (roleId: string): string => {
if (!props.roles || props.roles.length === 0) return '区县级' if (!props.roles || props.roles.length === 0) return '区县级'
const role = props.roles.find(r => r.id === roleId) const role = props.roles.find(r => r.id === roleId)
return role ? role.level : '区县级' return role ? role.level : '区县级'
} }
// 事件处理 // 事件处理
const handleSelect = (org: Organization) => { const handleSelect = (org: Organization) => {
if (isSelectable(org)) { if (isSelectable(org)) {
emit('select', org.id) emit('select', org.id)
} }
} }
const toggleExpand = (orgId: string) => { const toggleExpand = (orgId: string) => {
if (props.expandedIds && props.expandedIds.size > 0) { if (props.expandedIds && props.expandedIds.size > 0) {
// 使用父组件的展开状态 // 使用父组件的展开状态
...@@ -280,11 +259,9 @@ const toggleExpand = (orgId: string) => { ...@@ -280,11 +259,9 @@ const toggleExpand = (orgId: string) => {
} }
} }
} }
const handleToggleExpand = (orgId: string) => { const handleToggleExpand = (orgId: string) => {
emit('toggle-expand', orgId) emit('toggle-expand', orgId)
} }
// 初始化展开顶级组织 // 初始化展开顶级组织
if (props.expandedIds && props.expandedIds.size === 0) { if (props.expandedIds && props.expandedIds.size === 0) {
props.organizations.forEach(org => { props.organizations.forEach(org => {
...@@ -293,129 +270,101 @@ if (props.expandedIds && props.expandedIds.size === 0) { ...@@ -293,129 +270,101 @@ if (props.expandedIds && props.expandedIds.size === 0) {
} }
}) })
} }
</script>
</script>
<style scoped> <style scoped>
.space-y-1 > * + * { .space-y-1 > * + * {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.flex { .flex {
display: flex; display: flex;
} }
.items-center { .items-center {
align-items: center; align-items: center;
} }
.gap-2 { .gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
.p-1 { .p-1 {
padding: 0.25rem; padding: 0.25rem;
} }
.p-2 { .p-2 {
padding: 0.5rem; padding: 0.5rem;
} }
.rounded { .rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.cursor-not-allowed { .cursor-not-allowed {
cursor: not-allowed; cursor: not-allowed;
} }
.transition-colors { .transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
} }
.opacity-40 { .opacity-40 {
opacity: 0.4; opacity: 0.4;
} }
.bg-blue-50 { .bg-blue-50 {
background-color: #eff6ff; background-color: #eff6ff;
} }
.border { .border {
border-width: 1px; border-width: 1px;
} }
.border-blue-500 { .border-blue-500 {
border-color: #3b82f6; border-color: #3b82f6;
} }
.hover\:bg-neutral-50:hover { .hover\:bg-neutral-50:hover {
background-color: #f9fafb; background-color: #f9fafb;
} }
.hover\:bg-neutral-200:hover { .hover\:bg-neutral-200:hover {
background-color: #e5e7eb; background-color: #e5e7eb;
} }
.h-4 { .h-4 {
height: 1rem; height: 1rem;
} }
.w-4 { .w-4 {
width: 1rem; width: 1rem;
} }
.w-6 { .w-6 {
width: 1.5rem; width: 1.5rem;
} }
.text-neutral-300 { .text-neutral-300 {
color: #d1d5db; color: #d1d5db;
} }
.text-neutral-400 { .text-neutral-400 {
color: #9ca3af; color: #9ca3af;
} }
.text-neutral-500 { .text-neutral-500 {
color: #6b7280; color: #6b7280;
} }
.text-neutral-600 { .text-neutral-600 {
color: #4b5563; color: #4b5563;
} }
.text-neutral-900 { .text-neutral-900 {
color: #111827; color: #111827;
} }
.text-blue-600 { .text-blue-600 {
color: #2563eb; color: #2563eb;
} }
.text-xs { .text-xs {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
} }
.flex-1 { .flex-1 {
flex: 1 1 0%; flex: 1 1 0%;
} }
.ml-4 { .ml-4 {
margin-left: 1rem; margin-left: 1rem;
} }
.border-l-2 { .border-l-2 {
border-left-width: 2px; border-left-width: 2px;
} }
.border-neutral-200 { .border-neutral-200 {
border-color: #e5e7eb; border-color: #e5e7eb;
} }
</style> </style>
\ No newline at end of file \ No newline at end of file
<template> <template>
<div class="space-y-1"> <div class="space-y-1">
<div <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 <button
v-if="hasChildren" v-if="hasChildren"
...@@ -27,18 +26,18 @@ ...@@ -27,18 +26,18 @@
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-neutral-900">{{ permission.name }}</span> <span class="text-neutral-900">{{ permission.name }}</span>
<el-tag <!-- <el-tag
type="info" type="info"
effect="plain" effect="plain"
size="small" size="small"
class="text-xs" class="text-xs"
> >
{{ permission.code }} {{ permission.code }}
</el-tag> </el-tag> -->
</div> </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 }} {{ permission.description }}
</p> </p> -->
</div> </div>
</div> </div>
...@@ -103,9 +102,29 @@ const isSelected = computed(() => ...@@ -103,9 +102,29 @@ const isSelected = computed(() =>
const isIndeterminate = computed(() => { const isIndeterminate = computed(() => {
if (isSelected.value || !hasChildren.value) return false 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> </script>
......
...@@ -23,44 +23,28 @@ ...@@ -23,44 +23,28 @@
:data="filteredRoles" :data="filteredRoles"
style="width: 100%" style="width: 100%"
:header-cell-style="{ backgroundColor: '#f3f4f6', color: '#374151', fontWeight: '500', borderBottom: '1px solid #e5e7eb' }" :header-cell-style="{ backgroundColor: '#f3f4f6', color: '#374151', fontWeight: '500', borderBottom: '1px solid #e5e7eb' }"
:row-style="{ borderBottom: '1px solid rgb(243 244 246)' }" :row-style="{ borderBottom: '1px solid rgb(243 244 246)' }">
> <el-table-column prop="roleName" label="角色名称" min-width="120">
<el-table-column prop="name" label="角色名称" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="text-neutral-900">{{ row.name }}</span> <span class="text-neutral-900">{{ row.roleName }}</span>
</template>
</el-table-column>
<el-table-column prop="permissionIds" label="权限数量" min-width="100">
<template #default="{ row }">
<el-tag
type="info"
effect="plain"
size="small"
class="bg-neutral-50"
>
{{ row.permissionIds.length }} 个权限
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" min-width="80"> <el-table-column prop="status" label="状态" min-width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-tag <el-tag
:type="row.status === '启用' ? 'success' : 'info'" :type="row.status == 1 ? 'success' : 'info'"
effect="plain" effect="plain"
size="small" size="small"
> >
{{ row.status }} {{ row.status==1?'启用':'停用' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="150"> <el-table-column prop="createTime" label="创建时间" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
<span class="text-neutral-600">{{ row.createTime || '-' }}</span> <span class="text-neutral-600">{{ $utils.detailTime(row.createTime)}}</span>
</template> </template>
</el-table-column> </el-table-column>
...@@ -132,6 +116,8 @@ ...@@ -132,6 +116,8 @@
</div> </div>
<el-switch <el-switch
v-model="roleStatusEnabled" v-model="roleStatusEnabled"
active-text="开启"
inactive-text="关闭"
/> />
</div> </div>
</div> </div>
...@@ -142,13 +128,13 @@ ...@@ -142,13 +128,13 @@
<label class="block text-sm font-medium text-neutral-900"> <label class="block text-sm font-medium text-neutral-900">
权限选择 <span class="text-red-500">*</span> 权限选择 <span class="text-red-500">*</span>
</label> </label>
<el-tag <!-- <el-tag
type="primary" type="primary"
effect="plain" effect="plain"
class="bg-brand-primary/10 text-brand-primary border-brand-primary" class="bg-brand-primary/10 text-brand-primary border-brand-primary"
> >
已选择 {{ selectedPermissions.length }} 个权限 已选择 {{ selectedPermissions.length }} 个权限
</el-tag> </el-tag> -->
</div> </div>
<div class="border border-neutral-300 rounded p-4 bg-neutral-50 max-h-96 overflow-y-auto"> <div class="border border-neutral-300 rounded p-4 bg-neutral-50 max-h-96 overflow-y-auto">
...@@ -191,11 +177,32 @@ ...@@ -191,11 +177,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref,onMounted, computed, watch ,getCurrentInstance} from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import PermissionTreeNode from './PermissionTreeNode.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 { export interface Permission {
id: string id: string
...@@ -207,17 +214,18 @@ export interface Permission { ...@@ -207,17 +214,18 @@ export interface Permission {
} }
export interface Role { export interface Role {
id: string id: Number
name: string name: string
description?: string roleName?: string
level: RoleLevel level: '地市级' | '区县级' | '一线人员'
permissionIds: string[]
status: RoleStatus status: RoleStatus
remark?: string
permissionIds?: string[]
permissions?: string[]
createTime?: string createTime?: string
} }
export type RoleLevel = '地市级' | '区县级' | '一线人员' export type RoleStatus = 1 | 0
export type RoleStatus = '启用' | '禁用'
// Props // Props
interface Props { interface Props {
...@@ -242,50 +250,101 @@ const roleName = ref('') ...@@ -242,50 +250,101 @@ const roleName = ref('')
const roleDescription = ref('') const roleDescription = ref('')
const selectedPermissions = ref<string[]>([]) const selectedPermissions = ref<string[]>([])
const roleLevel = ref<'地市级' | '区县级' | '一线人员'>('区县级')
const roleStatusEnabled = ref(true) const roleStatusEnabled = ref(true)
// 权限树展开状态 // 权限树展开状态
const expandedPermissions = ref<Set<string>>(new Set()) const expandedPermissions = ref<Set<string>>(new Set())
// 计算属性 // 计算属性
const roleStatus = computed((): RoleStatus => roleStatusEnabled.value ? '启用' : '禁用') const roleStatus = computed((): RoleStatus => roleStatusEnabled.value ? 1 : 0)
const topLevelPermissions = computed(() => const topLevelPermissions = computed(() =>
props.permissions.filter(p => !p.parentId) props.permissions.filter(p => !p.parentId)
) )
const filteredRoles = computed(() => const filteredRoles = ref([])
props.roles.filter(role => role.name !== '地市主管理员')
) 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 = () => { const handleOpenAddDialog = () => {
editingRole.value = null editingRole.value = null
roleName.value = '' roleName.value = ''
roleDescription.value = '' roleDescription.value = ''
roleStatusEnabled.value = true
selectedPermissions.value = [] selectedPermissions.value = []
roleStatusEnabled.value = true
expandedPermissions.value = new Set(props.permissions.filter(p => !p.parentId).map(p => p.id)) expandedPermissions.value = new Set(props.permissions.filter(p => !p.parentId).map(p => p.id))
isDialogOpen.value = true isDialogOpen.value = true
} }
// 打开编辑对话框 // 打开编辑对话框
const handleOpenEditDialog = (role: Role) => { const handleOpenEditDialog = async (role: Role) => {
editingRole.value = role editingRole.value = role
roleName.value = role.name roleName.value = role.roleName || ''
roleDescription.value = role.description || '' 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)) 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 isDialogOpen.value = true
} }
// 保存角色 // 保存角色
const handleSave = () => { const handleSave = async () => {
if (!roleName.value.trim()) { if (!roleName.value.trim()) {
ElMessage.error('请输入角色名称') ElMessage.error('请输入角色名称')
return return
...@@ -296,25 +355,41 @@ const handleSave = () => { ...@@ -296,25 +355,41 @@ const handleSave = () => {
return return
} }
const roleData = { const response = await $api.createOrUpdateRole({
name: roleName.value.trim(), roleId: editingRole.value?editingRole.value.id:'',
description: roleDescription.value.trim(), roleName: roleName.value.trim(),
remark: roleDescription.value.trim(),
permissionIds: selectedPermissions.value, status: roleStatus.value,
status: roleStatus.value functionIds: selectedPermissions.value
} })
if (response.c === 0) {
if (editingRole.value) { handleFilter()
emit('updateRole', editingRole.value.id, roleData) ElMessage.success(editingRole.value?'编辑成功':'创建成功');
ElMessage.success('角色更新成功')
} else { } else {
emit('addRole', roleData) ElMessage.error(response.m);
ElMessage.success('角色创建成功')
} }
isDialogOpen.value = false 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 handleTogglePermission = (permissionId: string) => {
const permission = findPermissionById(permissionId) const permission = findPermissionById(permissionId)
...@@ -330,8 +405,18 @@ const handleTogglePermission = (permissionId: string) => { ...@@ -330,8 +405,18 @@ const handleTogglePermission = (permissionId: string) => {
}) })
newSelected = newSelected.filter(id => id !== permissionId) newSelected = newSelected.filter(id => id !== permissionId)
} else { } else {
// 选择:自动选择所有父权限 // 选择:自动选择所有父权限和所有子权限
newSelected.push(permissionId) newSelected.push(permissionId)
// 自动选择所有子权限
const childIds = getAllChildPermissionIds(permissionId)
childIds.forEach(id => {
if (!newSelected.includes(id)) {
newSelected.push(id)
}
})
// 自动选择所有父权限
let current = permission let current = permission
while (current.parentId) { while (current.parentId) {
if (!newSelected.includes(current.parentId)) { if (!newSelected.includes(current.parentId)) {
...@@ -342,6 +427,8 @@ const handleTogglePermission = (permissionId: string) => { ...@@ -342,6 +427,8 @@ const handleTogglePermission = (permissionId: string) => {
} }
selectedPermissions.value = newSelected selectedPermissions.value = newSelected
console.log(newSelected)
} }
// 查找权限 // 查找权限
......
<script setup lang="ts"> <script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue' // import WelcomeItem from './WelcomeItem.vue' // 文件不存在,暂时注释
import DocumentationIcon from './icons/IconDocumentation.vue' import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue' import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.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' ...@@ -12,6 +12,14 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue' import App from './App.vue'
import router from './router' 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) const app = createApp(App)
// 注册Element Plus图标 // 注册Element Plus图标
...@@ -23,4 +31,9 @@ app.use(createPinia()) ...@@ -23,4 +31,9 @@ app.use(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
// 将 API 接口、请求实例、状态管理和工具方法挂载到全局
app.config.globalProperties.$api = api
app.config.globalProperties.$stores = stores
app.config.globalProperties.$utils = commonUtils
app.mount('#app') app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ 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({ ...@@ -10,9 +10,22 @@ export default defineConfig({
vue(), vue(),
// vueDevTools(), // vueDevTools(),
], ],
base: './',
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': 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!