fix:联调评论接口、新增流程管理列表模块页面
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -79,6 +79,7 @@ declare module 'vue' {
|
|||||||
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
|
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
|
||||||
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
||||||
StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
||||||
|
UserSelector: typeof import('./src/components/userSelector/index.vue')['default']
|
||||||
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
||||||
Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ interface addCommentProps {
|
|||||||
|
|
||||||
|
|
||||||
// 获取评论
|
// 获取评论
|
||||||
export const getComment = (params: commentProps) => {
|
export const getComment = (data: commentProps) => {
|
||||||
return request.get('/comment/getComment', { params });
|
return request.post('/communicate/v1/comment/list', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加评论-回复评论
|
// 添加评论-回复评论
|
||||||
export const addReplyComment = (data: addCommentProps) => {
|
export const addReplyComment = (data: addCommentProps) => {
|
||||||
return request.post('/comment/addComment', data)
|
return request.post('/communicate/v1/comment/add', data)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除评论
|
// 删除评论
|
||||||
export const deleteComment = (id: string) => {
|
export const deleteComment = (id: string) => {
|
||||||
return request.delete(`/comment/deleteComment/${id}`);
|
return request.delete(`/communicate/v1/comment/del/${id}`);
|
||||||
}
|
}
|
||||||
@@ -1,128 +1,89 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="mj-card-container mj-grid-container">
|
||||||
<div
|
<div
|
||||||
class="mj-card-container"
|
class="mj-card-item"
|
||||||
:style="containerStyle"
|
v-for="(card, index) in list"
|
||||||
|
:key="index"
|
||||||
|
@click="cardItemClick(card, index)"
|
||||||
>
|
>
|
||||||
<div
|
<slot name="cardCover" :item="card">
|
||||||
class="mj-card-standard"
|
|
||||||
v-for="(card,carIndex) in list"
|
|
||||||
:key="carIndex"
|
|
||||||
@click="cardItemClick(card, carIndex)"
|
|
||||||
>
|
|
||||||
<slot name="cardTip">
|
|
||||||
<div class="mj-card-standard-tip" :style="standardTopStyle"></div>
|
<div class="mj-card-standard-tip" :style="standardTopStyle"></div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<div class="mj-card-standard-content">
|
<div class="mj-card-standard-content">
|
||||||
<slot name="content"></slot>
|
<slot name="content" :item="card" :index="index"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.actions" class="mj-card-actions">
|
||||||
|
<slot name="actions" :item="card"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: 'CardItem' });
|
defineOptions({ name: "CardItem" });
|
||||||
const { list = [], maxColumns = 4, standardTopStyle } = defineProps<{
|
|
||||||
list: any[]
|
interface Props {
|
||||||
maxColumns?: number // 每行最大展示数量,默认 4
|
list: any[];
|
||||||
standardTopStyle?: string | Record<string, any> // 外部传入的顶部样式
|
standardTopStyle?: string | Record<string, any>;
|
||||||
}>()
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
list: () => [],
|
||||||
|
standardTopStyle: "",
|
||||||
|
});
|
||||||
|
|
||||||
// 向外抛出的事件
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
/**
|
(e: "card-click", payload: { item: any; index: number }): void;
|
||||||
* 卡片点击事件
|
}>();
|
||||||
* @param e 事件名
|
|
||||||
* @param payload 点击的卡片数据和索引
|
|
||||||
*/
|
|
||||||
(e: 'card-click', payload: { item: any; index: number }): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 根据屏幕宽度和最大列数计算实际列数
|
|
||||||
const screenWidth = ref(window.innerWidth)
|
|
||||||
|
|
||||||
const containerStyle = computed(() => {
|
|
||||||
let cols = maxColumns
|
|
||||||
|
|
||||||
// 响应式调整
|
|
||||||
if (screenWidth.value <= 640) {
|
|
||||||
cols = 1
|
|
||||||
} else if (screenWidth.value <= 900) {
|
|
||||||
cols = Math.min(maxColumns, 2)
|
|
||||||
} else if (screenWidth.value <= 1200) {
|
|
||||||
cols = Math.min(maxColumns, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 卡片点击事件,向外抛出当前卡片数据和索引
|
|
||||||
const cardItemClick = (item: any, index: number) => {
|
const cardItemClick = (item: any, index: number) => {
|
||||||
emits('card-click', { item, index })
|
emits("card-click", { item, index });
|
||||||
}
|
};
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
const handleResize = () => {
|
|
||||||
screenWidth.value = window.innerWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mj-card-container {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.mj-card-standard {
|
<style lang="scss" scoped>
|
||||||
--radius: 16px;
|
.mj-grid-container {
|
||||||
--padding-standard: 20px;
|
// 卡片主体样式
|
||||||
--border-color: #e5e7eb;
|
.mj-card-item {
|
||||||
--background-color: #fff;
|
--radius: 12px;
|
||||||
--tw-shadow: 0 8px 12px #0000001a, 0 0px 12px #0000001a;
|
--primary-color: #409eff;
|
||||||
background-color: var(--background-color);
|
--border-color: #e4e7ed;
|
||||||
min-height: 140px;
|
|
||||||
width: 100%;
|
display: flex;
|
||||||
box-sizing: border-box;
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
transition: box-shadow 0.2s ease;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
// 丝滑动画过渡
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--tw-shadow);
|
box-shadow: 0 12px 24px rgba(64, 158, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-card-standard-tip {
|
.mj-card-standard-tip {
|
||||||
--height: 8px;
|
height: 8px;
|
||||||
height: var(--height);
|
background-color: var(--primary-color);
|
||||||
border-top-left-radius: inherit;
|
|
||||||
border-top-right-radius: inherit;
|
|
||||||
background-color: red;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-card-standard-content {
|
.mj-card-standard-content {
|
||||||
padding: var(--padding-standard);
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 平板:调整间距
|
.mj-card-actions {
|
||||||
@media (max-width: 900px) {
|
padding: 12px 16px;
|
||||||
.mj-card-container {
|
border-top: 1px solid #f0f0f0;
|
||||||
gap: 14px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端:调整间距
|
// 封面图内部图片的平滑过渡
|
||||||
@media (max-width: 640px) {
|
:deep(img) {
|
||||||
.mj-card-container {
|
transition: transform 0.5s ease;
|
||||||
gap: 12px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
v-for="(item, index) in visibleItems"
|
v-for="(item, index) in visibleItems"
|
||||||
:key="item[itemMap.id]"
|
:key="item[itemMap.id]"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: modelValue === item[itemMap.id] }"
|
:class="{ 'active': modelValue === item[itemMap.id] }"
|
||||||
@click="$emit('update:modelValue', item[itemMap.id])"
|
@click="$emit('update:modelValue', item[itemMap.id])"
|
||||||
>
|
>
|
||||||
<span class="tab-text">{{ item[itemMap.label] }}</span>
|
<span class="tab-text">{{ item[itemMap.label] }}</span>
|
||||||
@@ -106,8 +106,6 @@ const calculateLayout = () => {
|
|||||||
|
|
||||||
for (let i = 0; i < itemWidths.value.length; i++) {
|
for (let i = 0; i < itemWidths.value.length; i++) {
|
||||||
const w = itemWidths.value[i];
|
const w = itemWidths.value[i];
|
||||||
// 加上 20px 的 padding 补偿 (对应你 CSS 里的 padding: 0 20px)
|
|
||||||
// 最好在 measureWidths 阶段就包含 padding,或者在这里统一加
|
|
||||||
const fullWidth = w;
|
const fullWidth = w;
|
||||||
|
|
||||||
if (currentWidth + fullWidth > containerWidth) {
|
if (currentWidth + fullWidth > containerWidth) {
|
||||||
@@ -164,8 +162,6 @@ onMounted(async () => {
|
|||||||
updateActiveBar();
|
updateActiveBar();
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
// 尽量不要在 Resize 里用太长的 debounce,
|
|
||||||
// 否则你会感觉 Tab 是“跳”出来的,而不是“滑”出来的
|
|
||||||
handleResize();
|
handleResize();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,6 +186,9 @@ watch(() => props.items, async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.tabs-outer-container {
|
.tabs-outer-container {
|
||||||
|
--more-left-line:#f0f2f5;
|
||||||
|
--item-color:#9DA1B9;
|
||||||
|
--item-size:12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -209,7 +208,7 @@ watch(() => props.items, async () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%; // 确保占满父级
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@@ -218,38 +217,37 @@ watch(() => props.items, async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #606266;
|
color: var(--item-color);
|
||||||
font-size: 14px;
|
font-size: var(--item-size);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #409eff;
|
color: var(--el-color-primary);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-more-active {
|
&.is-more-active {
|
||||||
color: #409eff;
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-trigger {
|
.more-trigger {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
border-left: 1px solid #f0f2f5;
|
border-left: 1px solid var( --more-left-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-bar {
|
.active-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background-color: #409eff;
|
background-color: var(--el-color-primary);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.12s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
bottom: 0;
|
bottom: -1px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-active-item-overflow-tabs {
|
.is-active-item-overflow-tabs {
|
||||||
color: #409eff;
|
color: var(--el-color-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stage-breadcrumbs">
|
<div class="stage-breadcrumbs" :class="styleClass">
|
||||||
<div class="mj-panel-title">{{ title }}</div>
|
<div class="mj-panel-title">{{ title }}</div>
|
||||||
<div class="stage-breadcrumbs-content">
|
<div class="stage-breadcrumbs-content">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
@@ -12,13 +12,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "StageBreadcrumbs" });
|
defineOptions({ name: "StageBreadcrumbs" });
|
||||||
|
|
||||||
const { title } = defineProps<{
|
const { title,styleClass } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
styleClass: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.stage-breadcrumbs{
|
.stage-breadcrumbs{
|
||||||
padding: $mj-padding-standard 0;
|
padding: 0 0 $mj-padding-standard 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-bottom: 1px solid #E2E8F099;
|
border-bottom: 1px solid #E2E8F099;
|
||||||
@@ -48,4 +49,8 @@ const { title } = defineProps<{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stage-breadcrumbs-list{
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
17
src/components/userSelector/index.vue
Normal file
17
src/components/userSelector/index.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-selector-modules">
|
||||||
|
<name-avatar :name="name" :src="avatar" :size="size" style="margin-right: 12px" />
|
||||||
|
<span>{{ userAvatarName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import nameAvatar from "../nameAvatar/index.vue";
|
||||||
|
defineOptions({ name: "UserSelector" });
|
||||||
|
const props = defineProps({
|
||||||
|
userAvatarName: { type: String, default: "" },
|
||||||
|
avatar: { type: String, default: "" },
|
||||||
|
size: { type: Number, default: 24 },
|
||||||
|
name: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
// useTokenRefresh.ts
|
|
||||||
import { ref } from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
interface TokenData {
|
|
||||||
accessToken: string; // token内容
|
|
||||||
refreshToken: string; // 刷新的token内容
|
|
||||||
expiresAt?: number; // 过期时间戳
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useTokenRefresh(baseUrl: string) {
|
|
||||||
let refreshPromise: Promise<TokenData> | null = null;
|
|
||||||
let lastRefreshError: any = null;
|
|
||||||
// 当前Token数据(使用localStorage持久化) 可根据实际情况改成localStorage存储
|
|
||||||
const tokenData = ref<TokenData>({
|
|
||||||
accessToken: localStorage.getItem("accessToken") || "",
|
|
||||||
refreshToken: localStorage.getItem("refreshToken") || "",
|
|
||||||
expiresAt: Number(localStorage.getItem("expiresAt")) || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 核心刷新方法
|
|
||||||
const refreshToken = async (): Promise<TokenData> => {
|
|
||||||
if (refreshPromise) return refreshPromise;
|
|
||||||
|
|
||||||
// 如果上次刷新失败,短时间内不重复刷新,直接抛错
|
|
||||||
if (lastRefreshError) {
|
|
||||||
return Promise.reject(lastRefreshError);
|
|
||||||
}
|
|
||||||
const plainClient = axios.create({
|
|
||||||
baseURL: baseUrl,
|
|
||||||
timeout: 5 * 1000,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json;charset=utf-8",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const refreshToken = getRefreshToken();
|
|
||||||
if (!refreshToken) throw new Error("refreshToken is null");
|
|
||||||
const res = await plainClient.post("/auth/refresh", {
|
|
||||||
refreshToken,
|
|
||||||
});
|
|
||||||
const newTokenData: TokenData = {
|
|
||||||
accessToken: res.data.accessToken,
|
|
||||||
refreshToken: res.data.refreshToken,
|
|
||||||
expiresAt: Date.now() + res.data.expiresIn * 1000,
|
|
||||||
};
|
|
||||||
setTokens(newTokenData);
|
|
||||||
lastRefreshError = null; // 成功清除错误
|
|
||||||
return newTokenData;
|
|
||||||
} catch (error) {
|
|
||||||
// 刷新失败处理
|
|
||||||
lastRefreshError = error;
|
|
||||||
clearTokens();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
refreshPromise = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return refreshPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存token
|
|
||||||
const setTokens = (data: TokenData) => {
|
|
||||||
tokenData.value = data;
|
|
||||||
localStorage.setItem("accessToken", data.accessToken);
|
|
||||||
localStorage.setItem("refreshToken", data.refreshToken);
|
|
||||||
localStorage.setItem("expiresAt", data.expiresAt.toString());
|
|
||||||
};
|
|
||||||
// 清理Token
|
|
||||||
const clearTokens = () => {
|
|
||||||
localStorage.removeItem("accessToken");
|
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
localStorage.removeItem("expiresAt");
|
|
||||||
tokenData.value = { accessToken: "", refreshToken: "", expiresAt: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取当前Token
|
|
||||||
const getAccessToken = () => tokenData.value.accessToken;
|
|
||||||
// 获取refreshToken
|
|
||||||
const getRefreshToken = () => tokenData.value.refreshToken;
|
|
||||||
return {
|
|
||||||
refreshToken,
|
|
||||||
getRefreshToken,
|
|
||||||
setTokens,
|
|
||||||
getAccessToken,
|
|
||||||
clearTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 登录相关 Mock 数据
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock 登录响应
|
|
||||||
*/
|
|
||||||
export const getMockLoginResponse = (username: string, password: string) => {
|
|
||||||
// 模拟登录验证
|
|
||||||
if (username && password) {
|
|
||||||
return {
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
accessToken: 'mock_access_token_' + Date.now(),
|
|
||||||
refreshToken: 'mock_refresh_token_' + Date.now(),
|
|
||||||
expiresIn: 7200, // 2小时
|
|
||||||
userInfo: {
|
|
||||||
id: 1,
|
|
||||||
username: username,
|
|
||||||
name: username === 'admin' ? '管理员' : '普通用户',
|
|
||||||
role: username === 'admin' ? 'admin' : 'user',
|
|
||||||
avatar: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
msg: '登录成功',
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
code: 400,
|
|
||||||
data: null,
|
|
||||||
msg: '用户名或密码不能为空',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock 用户信息响应
|
|
||||||
*/
|
|
||||||
export const getMockUserInfoResponse = () => {
|
|
||||||
return {
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
id: 1,
|
|
||||||
username: 'admin',
|
|
||||||
name: '管理员',
|
|
||||||
role: 'admin',
|
|
||||||
avatar: '',
|
|
||||||
},
|
|
||||||
msg: '获取用户信息成功',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mock 数据管理
|
|
||||||
* 用于开发阶段模拟后端接口返回的数据
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getMockMenuResponse } from './menu'
|
|
||||||
import { getMockLoginResponse, getMockUserInfoResponse } from './auth'
|
|
||||||
|
|
||||||
// 是否启用 Mock(可以通过环境变量控制)
|
|
||||||
export const ENABLE_MOCK = import.meta.env.VITE_USE_MOCK === 'true' || import.meta.env.DEV
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock API 响应映射
|
|
||||||
* key: API 路径
|
|
||||||
* value: 返回的 Mock 数据函数
|
|
||||||
*/
|
|
||||||
const mockApiMap: Record<string, (params?: any, data?: any) => any> = {
|
|
||||||
'/api/menus': () => getMockMenuResponse(),
|
|
||||||
'/api/auth/login': (_params?: any, data?: any) => {
|
|
||||||
return getMockLoginResponse(data?.username || '', data?.password || '')
|
|
||||||
},
|
|
||||||
'/api/user/info': () => getMockUserInfoResponse(),
|
|
||||||
// 可以在这里添加更多的 mock 接口
|
|
||||||
// '/api/user/list': getMockUserList,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Mock 数据
|
|
||||||
* @param url API 路径
|
|
||||||
* @param params 请求参数(GET)
|
|
||||||
* @param data 请求体(POST/PUT)
|
|
||||||
* @returns Mock 数据或 null
|
|
||||||
*/
|
|
||||||
export const getMockData = (url: string, params?: any, data?: any): any => {
|
|
||||||
if (!ENABLE_MOCK || !url) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 精确匹配
|
|
||||||
if (mockApiMap[url]) {
|
|
||||||
return mockApiMap[url](params, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊匹配(支持带查询参数的 URL)
|
|
||||||
const urlWithoutQuery = url.split('?')[0]
|
|
||||||
if (urlWithoutQuery && mockApiMap[urlWithoutQuery]) {
|
|
||||||
return mockApiMap[urlWithoutQuery](params, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否应该使用 Mock 数据
|
|
||||||
* @param url API 路径
|
|
||||||
* @returns 是否应该使用 Mock
|
|
||||||
*/
|
|
||||||
export const shouldUseMock = (url: string): boolean => {
|
|
||||||
if (!ENABLE_MOCK || !url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 精确匹配
|
|
||||||
if (mockApiMap[url]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊匹配(支持带查询参数的 URL)
|
|
||||||
const urlWithoutQuery = url.split('?')[0]
|
|
||||||
return !!(urlWithoutQuery && mockApiMap[urlWithoutQuery])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Mock 接口
|
|
||||||
* @param url API 路径
|
|
||||||
* @param mockFn Mock 数据函数
|
|
||||||
*/
|
|
||||||
export const addMockApi = (url: string, mockFn: (params?: any, data?: any) => any) => {
|
|
||||||
mockApiMap[url] = mockFn
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除 Mock 接口
|
|
||||||
* @param url API 路径
|
|
||||||
*/
|
|
||||||
export const removeMockApi = (url: string) => {
|
|
||||||
delete mockApiMap[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getMockData,
|
|
||||||
shouldUseMock,
|
|
||||||
addMockApi,
|
|
||||||
removeMockApi,
|
|
||||||
ENABLE_MOCK,
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -296,6 +296,45 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const mockBackendMenuData = [
|
||||||
|
{
|
||||||
|
"name": "字典管理",
|
||||||
|
"code": "dict",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
"metadata": null,
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "组织管理",
|
||||||
|
"code": "origanization",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
"metadata": null,
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "人员管理",
|
||||||
|
"code": "personnel",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
"metadata": null,
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "权限管理",
|
||||||
|
"code": "permission",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
"metadata": null,
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流程管理",
|
||||||
|
"code": "flow",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
"metadata": null,
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Mock 菜单数据的响应格式
|
* 获取 Mock 菜单数据的响应格式
|
||||||
* 模拟后端接口返回的数据结构
|
* 模拟后端接口返回的数据结构
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ $color-white: #fff;
|
|||||||
.sub-list-controls {
|
.sub-list-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 12px;
|
|
||||||
padding-left: 46px; // 与头像对齐的偏移量
|
padding-left: 46px; // 与头像对齐的偏移量
|
||||||
|
|
||||||
.expand-line {
|
.expand-line {
|
||||||
@@ -230,6 +229,12 @@ $color-white: #fff;
|
|||||||
background-color: rgba(64, 158, 255, 0.2);
|
background-color: rgba(64, 158, 255, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.observer-anchor{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 评论组件骨架屏
|
// 评论组件骨架屏
|
||||||
|
|||||||
@@ -51,14 +51,14 @@
|
|||||||
<!-- 这个地方需要添加 查看更多-收起 -->
|
<!-- 这个地方需要添加 查看更多-收起 -->
|
||||||
<div class="parent-node">
|
<div class="parent-node">
|
||||||
<name-avatar
|
<name-avatar
|
||||||
:name="item.employee.name"
|
:name="item.employee.username"
|
||||||
:src="item.employee.avatar"
|
:src="item.employee.avatar"
|
||||||
:size="36"
|
:size="36"
|
||||||
/>
|
/>
|
||||||
<div class="node-main">
|
<div class="node-main">
|
||||||
<!-- 当前用户信息展示 -->
|
<!-- 当前用户信息展示 -->
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<span class="nickname">{{ item.employee.name }}</span>
|
<span class="nickname">{{ item.employee.username }}</span>
|
||||||
<span class="createTime">{{
|
<span class="createTime">{{
|
||||||
formatTime(item.createTime)
|
formatTime(item.createTime)
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -78,30 +78,25 @@
|
|||||||
link
|
link
|
||||||
class="delete-btn"
|
class="delete-btn"
|
||||||
@click="deleteMainComment(item)"
|
@click="deleteMainComment(item)"
|
||||||
v-if="currentUser.id === item?.employee.id"
|
v-if="currentUser.id === item?.employee.userId"
|
||||||
>
|
>
|
||||||
<el-icon><Delete /></el-icon> 删除
|
<el-icon><Delete /></el-icon> 删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 回复内容展示(二级-子集评论内容) -->
|
<!-- 回复内容展示(二级-子集评论内容) 通过id进行关联加载二级 -->
|
||||||
<div
|
<div
|
||||||
v-if="item.children?.length || item.localReplies?.length"
|
v-if="item.childrenCount || item.localReplies?.length"
|
||||||
class="sub-container"
|
class="sub-container"
|
||||||
>
|
>
|
||||||
<!-- 临时数据 -->
|
<!-- 临时数据 -->
|
||||||
<div
|
<div
|
||||||
v-for="replies in [
|
v-for="replies in item.children"
|
||||||
...(item.localReplies || []),
|
|
||||||
...(item.showAllReplies
|
|
||||||
? item.children
|
|
||||||
: item.children.slice(0, 1)),
|
|
||||||
]"
|
|
||||||
:key="replies.id"
|
:key="replies.id"
|
||||||
class="sub-node"
|
class="sub-node"
|
||||||
>
|
>
|
||||||
<name-avatar
|
<name-avatar
|
||||||
:name="replies.employee.name"
|
:name="replies.employee.username"
|
||||||
:src="replies.employee.avatar"
|
:src="replies.employee.avatar"
|
||||||
:size="36"
|
:size="36"
|
||||||
/>
|
/>
|
||||||
@@ -109,13 +104,12 @@
|
|||||||
<div class="sub-header">
|
<div class="sub-header">
|
||||||
<div class="sub-user-info">
|
<div class="sub-user-info">
|
||||||
<span class="nickname">{{
|
<span class="nickname">{{
|
||||||
replies.employee.name
|
replies.employee.username
|
||||||
}}</span>
|
}}</span>
|
||||||
<!-- TODO:这个地方判断 不是c-b这种回复就不展示 -->
|
<template v-if="replies.replyId && replies.replyId !== replies.employee.userId">
|
||||||
<template v-if="replies.reply">
|
|
||||||
<span class="reply-text">回复</span>
|
<span class="reply-text">回复</span>
|
||||||
<span class="target-name"
|
<span class="target-name"
|
||||||
>@{{ replies.reply.name }}</span
|
>@{{ replies.replyUser.username }}</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +130,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
class="delete-btn"
|
class="delete-btn"
|
||||||
v-if="currentUser.id === replies?.employee.id"
|
v-if="currentUser.id === replies?.employee.userId"
|
||||||
@click="deleteReply(replies, item)"
|
@click="deleteReply(replies, item)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
@@ -152,7 +146,8 @@
|
|||||||
<el-button
|
<el-button
|
||||||
v-if="!item.showAllReplies"
|
v-if="!item.showAllReplies"
|
||||||
link
|
link
|
||||||
@click="item.showAllReplies = true"
|
:loading="item.loading"
|
||||||
|
@click="handleExpand(item)"
|
||||||
>
|
>
|
||||||
展开 {{ item.childrenCount }} 条回复
|
展开 {{ item.childrenCount }} 条回复
|
||||||
<el-icon><ArrowDown /></el-icon>
|
<el-icon><ArrowDown /></el-icon>
|
||||||
@@ -162,7 +157,7 @@
|
|||||||
v-else-if="item.children.length < item.childrenCount"
|
v-else-if="item.children.length < item.childrenCount"
|
||||||
link
|
link
|
||||||
:loading="item.loading"
|
:loading="item.loading"
|
||||||
@click="loadReplies(item)"
|
@click="loadMoreReplies(item)"
|
||||||
>
|
>
|
||||||
更多
|
更多
|
||||||
{{ item.childrenCount - item.children.length }} 条回复
|
{{ item.childrenCount - item.children.length }} 条回复
|
||||||
@@ -173,8 +168,7 @@
|
|||||||
v-if="item.showAllReplies"
|
v-if="item.showAllReplies"
|
||||||
link
|
link
|
||||||
@click="collapseReplies(item)"
|
@click="collapseReplies(item)"
|
||||||
>
|
>收起 <el-icon><ArrowUp /></el-icon>
|
||||||
收起 <el-icon><ArrowUp /></el-icon>
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,6 +176,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 滚动加载元素 -->
|
||||||
|
<div ref="loadMoreAnchor" class="observer-anchor">
|
||||||
|
<el-icon v-if="infinityLoading" class="is-loading" size="20"><Loading/></el-icon>
|
||||||
|
<span v-else-if="noMore">没有更多评论了</span>
|
||||||
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,6 +218,7 @@
|
|||||||
<name-avatar
|
<name-avatar
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
:size="24"
|
:size="24"
|
||||||
|
:src="item.avatar"
|
||||||
style="margin-right: 10px"
|
style="margin-right: 10px"
|
||||||
/>
|
/>
|
||||||
<span class="mention-name">{{ item.name }}</span>
|
<span class="mention-name">{{ item.name }}</span>
|
||||||
@@ -264,6 +264,11 @@ import { useUserStore } from "@/store";
|
|||||||
import { useRelativeTime } from "@/hooks/useRelativeTime";
|
import { useRelativeTime } from "@/hooks/useRelativeTime";
|
||||||
import { useUserSearch } from "./useUserSearch";
|
import { useUserSearch } from "./useUserSearch";
|
||||||
import { parseMention } from "./utils";
|
import { parseMention } from "./utils";
|
||||||
|
import {
|
||||||
|
getComment,
|
||||||
|
addReplyComment,
|
||||||
|
deleteComment,
|
||||||
|
} from "@/api/modules/Comment";
|
||||||
const { formatTime } = useRelativeTime();
|
const { formatTime } = useRelativeTime();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -279,7 +284,7 @@ const props = defineProps({
|
|||||||
queryParams: {
|
queryParams: {
|
||||||
//外部传入的请求参数-获取评论
|
//外部传入的请求参数-获取评论
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({ instanceId: 1, moduleId: 1 }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@@ -293,88 +298,22 @@ const {
|
|||||||
} = useUserSearch((keyword, signal) => handleFetchSearch(keyword, signal));
|
} = useUserSearch((keyword, signal) => handleFetchSearch(keyword, signal));
|
||||||
|
|
||||||
// 评论业务逻辑
|
// 评论业务逻辑
|
||||||
const activeReply = reactive({ parentId: null, targetName: "" }); // 点击reply回复的数据信息
|
const activeReply = reactive({
|
||||||
|
replyUserId: "",
|
||||||
|
parentId: null,
|
||||||
|
targetName: "",
|
||||||
|
}); // 点击reply回复的数据信息
|
||||||
const mainInput = ref("");
|
const mainInput = ref("");
|
||||||
const loading = ref(true); //当前骨架屏显示
|
const loading = ref(true); //当前骨架屏显示
|
||||||
const expandingCount = ref(0); //展开收起统计
|
const expandingCount = ref(0); //展开收起统计
|
||||||
// 评论数据
|
// 评论数据
|
||||||
const commentData = ref([
|
const commentData = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
content: "这是我的测试评论数据信息@张三1 😄",
|
|
||||||
createTime: "2023-10-27T14:30:00",
|
|
||||||
mentions: [{
|
|
||||||
"id": 4,
|
|
||||||
"name": "张三1",
|
|
||||||
"start": 12,
|
|
||||||
"end": 16
|
|
||||||
}],
|
|
||||||
employee: {
|
|
||||||
id: 1,
|
|
||||||
name: "李星倩",
|
|
||||||
avatar: "",
|
|
||||||
},
|
|
||||||
childrenCount: 10,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
content: "好的那我来测试下艾特人员信息@冯娜 @张三1 你们好啊",
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "冯娜",
|
|
||||||
start: 14,
|
|
||||||
end: 17,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "张三1",
|
|
||||||
start: 18,
|
|
||||||
end: 22,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
employee: {
|
|
||||||
id: 1,
|
|
||||||
name: "李星倩",
|
|
||||||
avatar: "",
|
|
||||||
},
|
|
||||||
reply: {
|
|
||||||
id: 1,
|
|
||||||
name: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 102,
|
|
||||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
|
||||||
content: "收到,数据已入库,我马上看下。",
|
|
||||||
createTime: 1767604936684,
|
|
||||||
mentions: [{ userId: 2, name: "冯娜", start: 11, end: 15 }],
|
|
||||||
replyEmployee: {
|
|
||||||
id: 1,
|
|
||||||
name: "冯娜",
|
|
||||||
},
|
|
||||||
employee: {
|
|
||||||
id: 2,
|
|
||||||
name: "zhanghan",
|
|
||||||
avatar: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 103,
|
|
||||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
|
||||||
content: "收到,数据已入库,我马上看下。",
|
|
||||||
createTime: 1767604936684,
|
|
||||||
mentions: [{ userId: 2, name: "冯娜", start: 11, end: 15 }],
|
|
||||||
employee: {
|
|
||||||
id: 3,
|
|
||||||
name: "王五",
|
|
||||||
avatar: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// TODO:请求用户列表的接口函数
|
// 滚动加载
|
||||||
|
const infinityLoading = ref(false);
|
||||||
|
const loadMoreAnchor = ref(null);
|
||||||
|
const noMore = ref(false);
|
||||||
|
// FIXME:请求用户列表的接口函数
|
||||||
const handleFetchSearch = async (keyword, signal) => {
|
const handleFetchSearch = async (keyword, signal) => {
|
||||||
console.log("获取参数信息", keyword, signal);
|
console.log("获取参数信息", keyword, signal);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
@@ -392,6 +331,22 @@ const handleFetchSearch = async (keyword, signal) => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取当前的的评论信息
|
||||||
|
const getCommentData = async (childItem) => {
|
||||||
|
const queryData = {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
...props.queryParams,
|
||||||
|
...childItem,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await getComment(queryData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("comment error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 点击回复插入 mentions 块
|
// 点击回复插入 mentions 块
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === "Backspace") {
|
if (e.key === "Backspace") {
|
||||||
@@ -412,19 +367,20 @@ const handleKeyDown = (e) => {
|
|||||||
|
|
||||||
// 用户选中@圈人的操作
|
// 用户选中@圈人的操作
|
||||||
const onUserSelect = (user: any) => {
|
const onUserSelect = (user: any) => {
|
||||||
console.log("获取当前返回的用户信息:", user);
|
|
||||||
recordSelection(user);
|
recordSelection(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 回复
|
// 回复
|
||||||
const openReply = (target, group) => {
|
const openReply = (target, group) => {
|
||||||
// 1. 设置回复的目标关系
|
// 1. 设置回复的目标关系
|
||||||
activeReply.groupId = group.id; // 根评论ID
|
activeReply.groupId = target.rootId; // 根评论ID
|
||||||
activeReply.parentId = target.id; // 直接父级ID
|
activeReply.replyUserId = target.employee.userId; //回复-人的id
|
||||||
activeReply.targetName = target.employee.name;
|
activeReply.parentId = target.parentId; // 直接父级ID
|
||||||
|
activeReply.targetName = target.employee.username; //回复人员的username
|
||||||
|
activeReply.id = target.id;
|
||||||
|
|
||||||
// 2. 沿用 @ 逻辑:自动在输入框插入 @某人
|
// 2. 沿用 @ 逻辑:自动在输入框插入 @某人
|
||||||
const mentionStr = `@${target.employee.name} `;
|
const mentionStr = `@${target.employee.username} `;
|
||||||
|
|
||||||
// 如果输入框里已经有内容了,在前面追加,否则直接赋值
|
// 如果输入框里已经有内容了,在前面追加,否则直接赋值
|
||||||
if (!mainInput.value.includes(mentionStr)) {
|
if (!mainInput.value.includes(mentionStr)) {
|
||||||
@@ -433,13 +389,33 @@ const openReply = (target, group) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 删除回复-删除评论
|
// 删除回复-删除评论
|
||||||
const deleteReply = (target, group) => {
|
const deleteReply = async (target, group) => {
|
||||||
|
try {
|
||||||
|
// 删除成功
|
||||||
|
await deleteComment(target.id);
|
||||||
|
ElMessage.success("删除成功");
|
||||||
const index = group.children.findIndex((item) => item.id === target.id);
|
const index = group.children.findIndex((item) => item.id === target.id);
|
||||||
|
if (index !== -1) {
|
||||||
group.children.splice(index, 1);
|
group.children.splice(index, 1);
|
||||||
|
if (group.childrenCount > 0) {
|
||||||
|
group.childrenCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除后,如果子评论数量为0,则隐藏所有回复 childrenCount为0时会自动隐藏展开操作
|
||||||
|
if (group.childrenCount === 0) {
|
||||||
|
group.showAllReplies = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("删除失败", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除主评论-以及所有的子评论
|
// 删除主评论-以及所有的子评论
|
||||||
const deleteMainComment = (target) => {
|
const deleteMainComment = async (target) => {
|
||||||
|
try {
|
||||||
|
await deleteComment(target.id);
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
// 前端动态操作
|
||||||
const index = commentData.value.findIndex((item) => item.id === target.id);
|
const index = commentData.value.findIndex((item) => item.id === target.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
commentData.value.splice(index, 1);
|
commentData.value.splice(index, 1);
|
||||||
@@ -447,6 +423,9 @@ const deleteMainComment = (target) => {
|
|||||||
cancelReply();
|
cancelReply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("删除失败", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// emoji输入框选择
|
// emoji输入框选择
|
||||||
@@ -474,17 +453,18 @@ const handleSendComment = async () => {
|
|||||||
let rawText = mainInput.value;
|
let rawText = mainInput.value;
|
||||||
if (!rawText.trim()) return ElMessage.warning("内容不能为空");
|
if (!rawText.trim()) return ElMessage.warning("内容不能为空");
|
||||||
|
|
||||||
let finalParentId = null;
|
let finalReplyId = null;
|
||||||
const expectedPrefix = `@${activeReply.targetName} `;
|
const expectedPrefix = `@${activeReply.targetName} `;
|
||||||
const isActuallyReply =
|
const isActuallyReply =
|
||||||
activeReply.parentId && rawText.startsWith(expectedPrefix);
|
activeReply.replyUserId && rawText.startsWith(expectedPrefix);
|
||||||
if (isActuallyReply) {
|
if (isActuallyReply) {
|
||||||
finalParentId = activeReply.parentId;
|
finalReplyId = activeReply.replyUserId;
|
||||||
rawText = rawText.slice(expectedPrefix.length);
|
rawText = rawText.slice(expectedPrefix.length);
|
||||||
} else {
|
} else {
|
||||||
finalParentId = null;
|
finalReplyId = null;
|
||||||
}
|
}
|
||||||
const type = finalParentId ? "reply" : "main"; //ui构造渲染判断
|
|
||||||
|
const type = finalReplyId ? "reply" : "main"; //ui构造渲染判断
|
||||||
// 构造 Payload
|
// 构造 Payload
|
||||||
const mentionList: any[] = [];
|
const mentionList: any[] = [];
|
||||||
const localCache = new Map();
|
const localCache = new Map();
|
||||||
@@ -513,24 +493,23 @@ const handleSendComment = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 组装接口请求的参数
|
// 组装接口请求的参数
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
content: rawText,
|
content: rawText,
|
||||||
mentions: mentionList,
|
mentions: Object.keys(mentionList).length ? mentionList : null,
|
||||||
// TODO:如果是回复,带上关联 ID
|
|
||||||
reply: {
|
|
||||||
id: activeReply.groupId,
|
|
||||||
name: activeReply.groupId,
|
|
||||||
},
|
|
||||||
...props.queryParams,
|
...props.queryParams,
|
||||||
};
|
};
|
||||||
console.log("获取传递给后端的数据信息:", params);
|
// 回复数据复制
|
||||||
|
if(type === "reply"){
|
||||||
|
params.rootId = activeReply.groupId ? activeReply.groupId : activeReply.id;
|
||||||
|
params.parentId = activeReply.parentId;
|
||||||
|
params.replyUserId = activeReply.replyUserId;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 请求后端接口提交数据
|
// 请求后端接口提交数据
|
||||||
// const res = await api.postComment(params);
|
const res = await addReplyComment(params);
|
||||||
// 前端 UI 更新
|
// 前端 UI 更新
|
||||||
updateUIAfterSend(type, params);
|
updateUIAfterSend(type, params, res);
|
||||||
// 清空输入框
|
// 清空输入框
|
||||||
cancelReply();
|
cancelReply();
|
||||||
clearSelection();
|
clearSelection();
|
||||||
@@ -541,13 +520,18 @@ const handleSendComment = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 更新当前的UI层
|
// 更新当前的UI层
|
||||||
const updateUIAfterSend = (type, params) => {
|
const updateUIAfterSend = (type, params, response) => {
|
||||||
const newComment = {
|
const newComment = {
|
||||||
id: Date.now(), // TODO:这个地方需要替换为后端返回的真实id 用作后续的删除
|
id: response,
|
||||||
employee: {
|
employee: {
|
||||||
...currentUser.value,
|
...currentUser.value,
|
||||||
},
|
},
|
||||||
reply: params.reply,
|
replyId: params.replyUserId,
|
||||||
|
replyUser: {
|
||||||
|
userId: activeReply.replyUserId,
|
||||||
|
username: activeReply.targetName,
|
||||||
|
},
|
||||||
|
rootId: params.groupId,
|
||||||
content: params.content,
|
content: params.content,
|
||||||
mentions: params.mentionList,
|
mentions: params.mentionList,
|
||||||
createTime: new Date().valueOf(),
|
createTime: new Date().valueOf(),
|
||||||
@@ -559,87 +543,114 @@ const updateUIAfterSend = (type, params) => {
|
|||||||
commentData.value.unshift(newComment);
|
commentData.value.unshift(newComment);
|
||||||
} else {
|
} else {
|
||||||
//回复某人的数据渲染
|
//回复某人的数据渲染
|
||||||
const targetGroup = commentData.value.find((i) => i.id === params.reply.id);
|
const targetGroup = commentData.value.find(
|
||||||
|
(i) => i.id === params.rootId
|
||||||
|
); //获取返回的数据信息
|
||||||
if (targetGroup) {
|
if (targetGroup) {
|
||||||
newComment.replyTo = activeReply.targetName;
|
if (!targetGroup.children) targetGroup.children = [];
|
||||||
if (!targetGroup.localReplies) targetGroup.localReplies = [];
|
targetGroup.children.unshift(newComment);
|
||||||
targetGroup.localReplies.unshift(newComment);
|
targetGroup.childrenCount = targetGroup.children.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动加载数据
|
||||||
|
const setupObserver = () => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
// 如果探测器进入视口,且当前没在加载,且还有更多数据
|
||||||
|
if (
|
||||||
|
entries[0].isIntersecting &&
|
||||||
|
!infinityLoading.value &&
|
||||||
|
!noMore.value
|
||||||
|
) {
|
||||||
|
loadMainComments();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadMoreAnchor.value) {
|
||||||
|
observer.observe(loadMoreAnchor.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO:展开
|
const nextPage = ref(1);
|
||||||
const MOCK_REPLIES_POOL = Array.from({ length: 20 }, (_, i) => ({
|
const loadMainComments = async () => {
|
||||||
id: 200 + i,
|
infinityLoading.value = true;
|
||||||
content: `这是模拟的第 ${i + 1} 条回复内容,@张三 用于测试分页加载。`,
|
try {
|
||||||
createTime: Date.now() - i * 100000,
|
const res = await getCommentData({ pageNo: nextPage.value });
|
||||||
mentions: [{ id: 1, name: "张三", start: 3, end: 10 }],
|
const processedRecords = res.records.map(item => ({
|
||||||
employee: {
|
...item,
|
||||||
id: 10 + i,
|
children: item.children || [],
|
||||||
name: `同事${i + 1}`,
|
localReplies: [],
|
||||||
avatar: "",
|
loading: false,
|
||||||
},
|
showAllReplies: false,
|
||||||
reply: null,
|
currentPage: 1
|
||||||
}));
|
}));
|
||||||
const loadReplies = async (item) => {
|
commentData.value = [...commentData.value,...processedRecords];
|
||||||
if (item.loading) return;
|
if (nextPage.value >= res.totalPage) {
|
||||||
expandingCount.value++;
|
noMore.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
infinityLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO:展开 加载二级内容(包含回复)
|
||||||
|
const PAGE_SIZE_MORE = 3;
|
||||||
|
const handleExpand = async (item) => {
|
||||||
item.loading = true;
|
item.loading = true;
|
||||||
try {
|
try {
|
||||||
// 后端获取最终的数据信息
|
const response = await getCommentData({
|
||||||
// const res = await xxxxxx()
|
rootId: item.id,
|
||||||
// 模拟延迟
|
pageNo: 1,
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
pageSize: PAGE_SIZE_MORE,
|
||||||
const res = [];
|
});
|
||||||
|
|
||||||
// 模拟的数据
|
item.children = response.records; // 填充数据
|
||||||
const currentLength = item.children.length;
|
|
||||||
const pageSize = 3;
|
|
||||||
const nextBatch = MOCK_REPLIES_POOL.slice(
|
|
||||||
currentLength,
|
|
||||||
currentLength + pageSize
|
|
||||||
);
|
|
||||||
const combined = [
|
|
||||||
...(item.localReplies || []),
|
|
||||||
...res,
|
|
||||||
...item.children,
|
|
||||||
...nextBatch,
|
|
||||||
];
|
|
||||||
item.children = combined.filter(
|
|
||||||
(v, i, a) => a.findIndex((t) => t.id === v.id) === i
|
|
||||||
);
|
|
||||||
item.localReplies = [];
|
|
||||||
item.showAllReplies = true;
|
item.showAllReplies = true;
|
||||||
|
item.currentPage = 1; // 记录当前页码
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", error);
|
console.error("加载回复失败", error);
|
||||||
} finally {
|
} finally {
|
||||||
item.loading = false;
|
item.loading = false;
|
||||||
setTimeout(() => {
|
|
||||||
expandingCount.value--;
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 加载更多的页码
|
||||||
|
const loadMoreReplies = async (item) => {
|
||||||
|
item.loading = true;
|
||||||
|
const nextPage = (item.currentPage || 1) + 1;
|
||||||
|
try {
|
||||||
|
const res = await getCommentData({
|
||||||
|
rootId: item.id,
|
||||||
|
pageNum: nextPage,
|
||||||
|
pageSize: PAGE_SIZE_MORE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将新数据追加到列表末尾
|
||||||
|
item.children = [...item.children, ...res];
|
||||||
|
item.currentPage = nextPage;
|
||||||
|
} finally {
|
||||||
|
item.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
// 收起
|
// 收起
|
||||||
const collapseReplies = (item) => {
|
const collapseReplies = (item) => {
|
||||||
expandingCount.value++;
|
|
||||||
item.showAllReplies = false;
|
item.showAllReplies = false;
|
||||||
console.log("收起数据", expandingCount.value);
|
item.children = []; //清空数据
|
||||||
nextTick(() => {
|
item.currentPage = 0; //重置页码
|
||||||
const el = document.getElementById(`comment-${item.id}`);
|
|
||||||
el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
expandingCount.value--;
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化 (骨架屏展示)
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}, 500);
|
}, 300);
|
||||||
|
setupObserver();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,7 @@
|
|||||||
<!-- 右侧用户的内容 -->
|
<!-- 右侧用户的内容 -->
|
||||||
<rightMenuGroup @on-stage-manage="onStageManage" />
|
<rightMenuGroup @on-stage-manage="onStageManage" />
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main class="mj-main-backend-content">
|
||||||
<!-- <card-item :list="[1,2,3,4,5,6]"/> -->
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -218,6 +217,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
:deep(.el-main) {
|
:deep(.el-main) {
|
||||||
--el-main-padding: 16px;
|
--el-main-padding: 16px;
|
||||||
|
padding: var(--el-main-padding) calc(var(--el-main-padding) * 2);
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
|||||||
import { login } from "@/api";
|
import { login } from "@/api";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import TokenManager from '@/utils/storage';
|
import TokenManager from '@/utils/storage';
|
||||||
|
import { Lock,Message,Right } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
defineOptions({ name: "Login" });
|
defineOptions({ name: "Login" });
|
||||||
|
|
||||||
@@ -113,8 +114,8 @@ const loginFormRef = ref<FormInstance>();
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
username: "user",
|
username: "18280362106",
|
||||||
password: "password",
|
password: "123456789",
|
||||||
remember:false
|
remember:false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,9 +142,10 @@ const handleLogin = async () => {
|
|||||||
grant_type: 'password'
|
grant_type: 'password'
|
||||||
});
|
});
|
||||||
if (response) {
|
if (response) {
|
||||||
const { access_token, refresh_token, expires_in,username,userId,avatar } = response;
|
const { access_token, refresh_token, expires_in,username,userId,avatar,nickname } = response;
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
username,
|
username,
|
||||||
|
nickname,
|
||||||
userId,
|
userId,
|
||||||
avatar
|
avatar
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
ElMessage.error(error.msg || "登录失败,请稍后重试");
|
ElMessage.error(error.error || "登录失败,请稍后重试");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="mj-drawer-top-container">
|
<div class="mj-drawer-top-container">
|
||||||
<div class="top-toolbar">
|
<div class="top-toolbar">
|
||||||
<div class="left-actions">
|
<div class="left-actions">
|
||||||
<div class="search-dict-input">
|
<div class="search-auto-expand-input">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索字段名称..."
|
placeholder="搜索字段名称..."
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</CommonFilter>
|
</CommonFilter>
|
||||||
<div class="search-dict-input">
|
<div class="search-auto-expand-input">
|
||||||
<el-input
|
<el-input
|
||||||
placeholder="搜索字典..."
|
placeholder="搜索字典..."
|
||||||
class="auto-expand-input"
|
class="auto-expand-input"
|
||||||
|
|||||||
96
src/pages/stage/flow/flowCard.vue
Normal file
96
src/pages/stage/flow/flowCard.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flow-card">
|
||||||
|
<div class="card-cover">
|
||||||
|
<img src="https://via.placeholder.com/300x180" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="title-row">
|
||||||
|
<h3 class="mj-ellipsis-one-line title">商机通用跟进流程</h3>
|
||||||
|
<el-icon class="more-icon"><MoreFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="meta-info">
|
||||||
|
<span>创建人:程彬</span>
|
||||||
|
<span class="divider">|</span>
|
||||||
|
<span>2024-12-20</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { MoreFilled } from "@element-plus/icons-vue";
|
||||||
|
defineOptions({ name: "FlowCard" });
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flow-card {
|
||||||
|
--card-active-border-color: #409eff;
|
||||||
|
--card-radius:5px;
|
||||||
|
width: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce2e7;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--card-active-border-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-cover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 5px 12px;
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-icon {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #90a1b9;
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #dce2e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="mj-flow-container">
|
||||||
|
<stageBreadcrumbs title="SOP流程管理" styleClass="stage-breadcrumbs-list">
|
||||||
</div>
|
<template #content>
|
||||||
|
<OverflowTabs
|
||||||
|
:itemMap="{ id: 'id', label: 'name' }"
|
||||||
|
v-model="activeTab"
|
||||||
|
:items="tabList"
|
||||||
|
:height="60"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<div class="search-auto-expand-input">
|
||||||
|
<el-input
|
||||||
|
placeholder="搜索流程名称..."
|
||||||
|
class="auto-expand-input"
|
||||||
|
:prefix-icon="'Search'"
|
||||||
|
v-model="searchVal"
|
||||||
|
@keyup.enter="fetchTableData"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</stageBreadcrumbs>
|
||||||
|
<div class="mj-flow-content">
|
||||||
|
<subTabs v-model:tabs="tabsIndex" :menuList="menuList" />
|
||||||
|
<div class="mj-card-container mj-flow-card-container">
|
||||||
|
<div v-for="t in 5" :key="t" class="mj-flow-card-grid">
|
||||||
|
<flowCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {reactive,ref,onMounted} from "vue"
|
import subTabs from "./subTabs.vue";
|
||||||
|
import flowCard from "./flowCard.vue";
|
||||||
|
|
||||||
defineOptions({})
|
defineOptions({ name: "Flow" });
|
||||||
|
const activeTab = ref<string>(1);
|
||||||
|
const searchVal = ref<string>("");
|
||||||
|
|
||||||
|
const tabsIndex = ref(1);
|
||||||
|
const menuList = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "线索管理",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "客户管理",
|
||||||
|
icon: "Setting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "游戏与工作室",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "商机管理",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const tabList = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "商机",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "合同",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "项目",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "招聘",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const fetchTableData = () => {
|
||||||
|
console.log("fetchTableData");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
|||||||
55
src/pages/stage/flow/subTabs.vue
Normal file
55
src/pages/stage/flow/subTabs.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mj-subnav-container">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in menuList"
|
||||||
|
:key="index"
|
||||||
|
:class="['nav-item', { 'is-active': tabs === index }]"
|
||||||
|
@click="tabs = index"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "subTabs" });
|
||||||
|
const props = defineProps({
|
||||||
|
menuList: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = defineModel("tabs");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mj-subnav-container {
|
||||||
|
$primary-color: var(--el-color-primary);
|
||||||
|
$bg-active: #EFF6FF;
|
||||||
|
$text-idle: #90A1B9;
|
||||||
|
$text-active: var(--el-color-primary);
|
||||||
|
$border-active:#DEECFE;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 12px 0;
|
||||||
|
.nav-item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-idle;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.12s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: $text-active;
|
||||||
|
background-color: $bg-active;
|
||||||
|
border: 1px solid $border-active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="mj-organization">
|
<div class="mj-organization">
|
||||||
<!-- 顶部的tabs菜单 -->
|
<!-- 顶部的tabs菜单 -->
|
||||||
<div class="organization-tabs">
|
<div class="organization-tabs">
|
||||||
<stageBreadcrumbs title="组织管理">
|
<stageBreadcrumbs title="组织管理" style-class="stage-breadcrumbs-list">
|
||||||
<template #content>
|
<template #content>
|
||||||
<OverflowTabs :itemMap="{id:'id',label:'name'}" v-model="activeTab" :items="tabList" :height="60" />
|
<OverflowTabs :itemMap="{id:'id',label:'name'}" v-model="activeTab" :items="tabList" :height="60" />
|
||||||
</template>
|
</template>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
:color="getIconColor(node)"
|
:color="getIconColor(node)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AutoTooltip :content="node.label" class="tree-node-label" />
|
<AutoTooltip :content="node.label" class="mj-ellipsis-one-line tree-node-label" />
|
||||||
</div>
|
</div>
|
||||||
<div class="org-tree-item-right">
|
<div class="org-tree-item-right">
|
||||||
<el-icon :size="15" @click.stop="onOrgTreeDelete(node,data)">
|
<el-icon :size="15" @click.stop="onOrgTreeDelete(node,data)">
|
||||||
@@ -266,11 +266,6 @@ onMounted(()=>{
|
|||||||
@use "sass:math";
|
@use "sass:math";
|
||||||
.mj-organization {
|
.mj-organization {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.organization-tabs {
|
|
||||||
:deep(.stage-breadcrumbs) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mj-organization-card {
|
.mj-organization-card {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #e2e8f099;
|
border: 1px solid #e2e8f099;
|
||||||
@@ -325,9 +320,6 @@ onMounted(()=>{
|
|||||||
}
|
}
|
||||||
.tree-node-label{
|
.tree-node-label{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,163 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mj-permission-management">
|
<div class="mj-permission-management">
|
||||||
|
<stageBreadcrumbs title="权限管理" style-class="stage-breadcrumbs-list">
|
||||||
|
<template #content>
|
||||||
|
<OverflowTabs v-model="activeTab" :items="tabList" :height="60" />
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<div class="mj-permission-actions">
|
||||||
|
<div class="search-auto-expand-input">
|
||||||
|
<el-input
|
||||||
|
placeholder="搜索字典..."
|
||||||
|
class="auto-expand-input"
|
||||||
|
:prefix-icon="'Search'"
|
||||||
|
v-model="searchVal"
|
||||||
|
@keyup.enter="fetchTableData"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
<div class="mj-dict-actions-right">
|
||||||
|
<el-button :icon="'Plus'" type="primary" plain @click="addRoles">新增角色</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</stageBreadcrumbs>
|
||||||
|
|
||||||
</div>
|
<!-- 表格 -->
|
||||||
|
<CommonTable
|
||||||
|
ref="dictTableRef"
|
||||||
|
:columns="columns"
|
||||||
|
v-model:data="dataList"
|
||||||
|
v-model:total="total"
|
||||||
|
pagination
|
||||||
|
:request-api="getTableData"
|
||||||
|
>
|
||||||
|
</CommonTable>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import CommonTable from "@/components/proTable/index.vue";
|
||||||
|
defineOptions({ name: "PermissionManagement" });
|
||||||
|
const activeTab = ref(1);
|
||||||
|
const dictTableRef = ref(null);
|
||||||
|
const dataList = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
defineOptions({ name: "PermissionManagement"})
|
const tabList = [
|
||||||
|
{
|
||||||
|
label: "角色与权限",
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户管理",
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "权限配置",
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: "id", label: "编号", width: "80", align: "center", slot: "number" },
|
||||||
|
{ prop: "name", label: "字典名称", align: "center", slot: "name" },
|
||||||
|
{
|
||||||
|
prop: "key",
|
||||||
|
label: "成员数量",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "key",
|
||||||
|
label: "权限数量",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "status",
|
||||||
|
label: "状态",
|
||||||
|
align: "center",
|
||||||
|
slot: "status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "remark",
|
||||||
|
label: "角色类型",
|
||||||
|
align: "center",
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "remark",
|
||||||
|
label: "关联岗位",
|
||||||
|
align: "center",
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "createTime",
|
||||||
|
label: "更新时间",
|
||||||
|
align: "center",
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (val) => {
|
||||||
|
return val.createTime
|
||||||
|
? dayjs(val.createTime).format("YYYY-MM-DD HH:mm")
|
||||||
|
: "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updateByName",
|
||||||
|
label: "更新人",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "actions",
|
||||||
|
label: "操作",
|
||||||
|
align: "right",
|
||||||
|
width: "300",
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: "权限",
|
||||||
|
type: "primary",
|
||||||
|
link: true,
|
||||||
|
permission: ["edit"],
|
||||||
|
onClick: (row) => handleEdit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "复制",
|
||||||
|
type: "default",
|
||||||
|
link: true,
|
||||||
|
permission: ["edit"],
|
||||||
|
onClick: (row) => handleEdit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "编辑",
|
||||||
|
type: "default",
|
||||||
|
link: true,
|
||||||
|
permission: ["config"],
|
||||||
|
onClick: (row) => handlefieldsConfig(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "删除",
|
||||||
|
type: "danger",
|
||||||
|
link: true,
|
||||||
|
permission: ["delete"],
|
||||||
|
onClick: (row) => handleDelete(row),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 请求数据信息
|
||||||
|
const fetchTableData = () => {};
|
||||||
|
|
||||||
|
// 新增角色
|
||||||
|
const addRoles = () => {};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.mj-permission-management {
|
||||||
|
:deep(.stage-breadcrumbs) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.mj-permission-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,16 +3,16 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
|||||||
import { ElNotification } from "element-plus";
|
import { ElNotification } from "element-plus";
|
||||||
import { VITE_APP_BASE_API } from "../../config.js";
|
import { VITE_APP_BASE_API } from "../../config.js";
|
||||||
import TokenManager from "@/utils/storage";
|
import TokenManager from "@/utils/storage";
|
||||||
import { getMockData, shouldUseMock } from "@/mock"; //mock数据信息
|
|
||||||
const tokenManager = TokenManager.getInstance();
|
const tokenManager = TokenManager.getInstance();
|
||||||
const baseUrl = import.meta.env.MODE === "development" ? "/api" : VITE_APP_BASE_API;
|
const baseUrl =
|
||||||
|
import.meta.env.MODE === "development" ? "/api" : VITE_APP_BASE_API;
|
||||||
// 1. 锁和队列定义在类外部,确保全局唯一
|
// 1. 锁和队列定义在类外部,确保全局唯一
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
let requestsQueue: Array<(token: string) => void> = [];
|
let requestsQueue: Array<(token: string) => void> = [];
|
||||||
|
|
||||||
// 登录接口 传递参数不一样
|
// 登录接口 传递参数不一样
|
||||||
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
||||||
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:secret");
|
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:H7mHCOfjhV2VqlR31OHi6ruVMeOQvluz");
|
||||||
|
|
||||||
class HttpRequest {
|
class HttpRequest {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
@@ -127,13 +127,23 @@ class HttpRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 其它业务错误
|
// 其它业务错误
|
||||||
ElNotification.error({ title: "提示", message: res.msg || "服务异常" });
|
ElNotification.error({
|
||||||
|
title: "提示",
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
message: `${res.msg}<br/>错误码:${res.subCode}` || "服务异常",
|
||||||
|
});
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 网络层错误处理
|
// 网络层错误处理
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
this.clearTokens();
|
this.clearTokens();
|
||||||
|
} else {
|
||||||
|
if(this.isAuthEndpoint(error.config.url)){
|
||||||
|
ElNotification.error({ title: "提示", message: error.response.data.error || '未知错误' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElNotification.error({ title: "提示", message: "服务异常" });
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import TokenManager from "@/utils/storage";
|
|||||||
|
|
||||||
import Login from "@/pages/Login/index.vue";
|
import Login from "@/pages/Login/index.vue";
|
||||||
import HomeView from "@/pages/Layout/index.vue";
|
import HomeView from "@/pages/Layout/index.vue";
|
||||||
|
import { mockBackendMenuData } from "@/mock/menu";
|
||||||
const tokenManager = TokenManager.getInstance();
|
const tokenManager = TokenManager.getInstance();
|
||||||
// 基础路由(不需要权限验证)
|
// 基础路由(不需要权限验证)
|
||||||
const constantRoutes: RouteRecordRaw[] = [
|
const constantRoutes: RouteRecordRaw[] = [
|
||||||
@@ -143,44 +143,7 @@ const addDynamicRoutes = async () => {
|
|||||||
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||||
let allRoutes:any[] = [];
|
let allRoutes:any[] = [];
|
||||||
if (userStore.isBackendUser) {
|
if (userStore.isBackendUser) {
|
||||||
// const backendResponse = await getRouteMenus();
|
const backendResponse = await getRouteMenus();
|
||||||
const backendResponse = [
|
|
||||||
{
|
|
||||||
"name": "字典管理",
|
|
||||||
"code": "dict",
|
|
||||||
"icon": "OfficeBuilding",
|
|
||||||
"metadata": null,
|
|
||||||
"children": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "组织管理",
|
|
||||||
"code": "origanization",
|
|
||||||
"icon": "OfficeBuilding",
|
|
||||||
"metadata": null,
|
|
||||||
"children": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "人员管理",
|
|
||||||
"code": "personnel",
|
|
||||||
"icon": "OfficeBuilding",
|
|
||||||
"metadata": null,
|
|
||||||
"children": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "权限管理",
|
|
||||||
"code": "permission",
|
|
||||||
"icon": "OfficeBuilding",
|
|
||||||
"metadata": null,
|
|
||||||
"children": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "流程管理",
|
|
||||||
"code": "flow",
|
|
||||||
"icon": "OfficeBuilding",
|
|
||||||
"metadata": null,
|
|
||||||
"children": null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
allRoutes = [
|
allRoutes = [
|
||||||
{
|
{
|
||||||
code: "stage",
|
code: "stage",
|
||||||
@@ -189,7 +152,7 @@ const addDynamicRoutes = async () => {
|
|||||||
meta:{
|
meta:{
|
||||||
title:'管理中心'
|
title:'管理中心'
|
||||||
},
|
},
|
||||||
children: backendResponse,
|
children: Object.keys(backendResponse).length ? backendResponse : mockBackendMenuData,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// popover 筛选框的全局样式
|
||||||
.filter-popper.el-popover.el-popper {
|
.filter-popper.el-popover.el-popper {
|
||||||
--el-popover-padding: 0;
|
--el-popover-padding: 0;
|
||||||
border-radius: var(--mj-popper-radius);
|
border-radius: var(--mj-popper-radius);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局重新element相关样式
|
// 全局重写element相关样式
|
||||||
.mj-input-form {
|
.mj-input-form {
|
||||||
.el-input {
|
.el-input {
|
||||||
--el-border-radius-base: 10px;
|
--el-border-radius-base: 10px;
|
||||||
@@ -38,7 +39,7 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
// 搜索框动画
|
// 搜索框动画
|
||||||
.search-dict-input {
|
.search-auto-expand-input {
|
||||||
--default-width: 160px;
|
--default-width: 160px;
|
||||||
--max-width: 224px;
|
--max-width: 224px;
|
||||||
width: var(--default-width);
|
width: var(--default-width);
|
||||||
@@ -71,7 +72,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 筛选框全局样式内容
|
// 筛选框内容的全局样式内容
|
||||||
.mj-filter-content {
|
.mj-filter-content {
|
||||||
min-width: 380px;
|
min-width: 380px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -133,3 +134,41 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 全局单行省略样式
|
||||||
|
.mj-ellipsis-one-line {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 卡片的全局样式
|
||||||
|
|
||||||
|
.mj-card-container{
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
// 默认:一行 5 列
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
// 1400px 以下:觉得 5 个太挤,切到一行 4 列
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
// 1100px 以下 (iPad横屏区间):一行 3 列
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
// 768px 以下 (iPad竖屏):一行 2 列
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
// 480px 以下 (手机端):一行 1 列
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user