2026-01-20 02:14:51 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="share-config-form">
|
|
|
|
|
|
<div class="share-config-content">
|
2026-04-28 22:02:15 +08:00
|
|
|
|
<div class="share-mode-cards" role="radiogroup" aria-label="共享设置">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="option in shareModeOptions"
|
|
|
|
|
|
:key="option.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="radio"
|
|
|
|
|
|
class="share-mode-card"
|
|
|
|
|
|
:class="{ active: config.is_shared === option.value }"
|
|
|
|
|
|
:aria-checked="config.is_shared === option.value"
|
|
|
|
|
|
:tabindex="config.is_shared === option.value ? 0 : -1"
|
|
|
|
|
|
@click="setShareMode(option.value)"
|
|
|
|
|
|
@keydown.enter.prevent="setShareMode(option.value)"
|
|
|
|
|
|
@keydown.space.prevent="setShareMode(option.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="card-icon-wrapper" aria-hidden="true">
|
|
|
|
|
|
<component :is="option.icon" class="card-icon" :size="20" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-content">
|
|
|
|
|
|
<div class="card-title">{{ option.title }}</div>
|
|
|
|
|
|
<div class="card-description">{{ option.description }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
2026-01-20 02:14:51 +08:00
|
|
|
|
</div>
|
2026-04-28 22:02:15 +08:00
|
|
|
|
<div v-if="!config.is_shared" class="compact-dept-selection">
|
|
|
|
|
|
<div class="dept-selection-header">
|
|
|
|
|
|
<div class="dept-selection-label">可访问部门</div>
|
|
|
|
|
|
<div class="dept-selection-hint">请选择可访问该知识库的部门</div>
|
|
|
|
|
|
</div>
|
2026-01-20 02:14:51 +08:00
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="config.accessible_department_ids"
|
|
|
|
|
|
mode="multiple"
|
2026-04-28 22:02:15 +08:00
|
|
|
|
placeholder="请选择部门"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="full-width dept-selection-input"
|
2026-01-20 02:14:51 +08:00
|
|
|
|
:options="departmentOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
2026-04-28 22:02:15 +08:00
|
|
|
|
import { Globe, Building2 } from 'lucide-vue-next'
|
2026-01-20 02:14:51 +08:00
|
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
|
|
|
import { departmentApi } from '@/apis/department_api'
|
|
|
|
|
|
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
const departments = ref([])
|
|
|
|
|
|
|
2026-04-28 22:02:15 +08:00
|
|
|
|
const shareModeOptions = [
|
|
|
|
|
|
{
|
|
|
|
|
|
value: true,
|
|
|
|
|
|
title: '公开给团队',
|
|
|
|
|
|
description: '团队内所有成员都可以访问这个知识库',
|
|
|
|
|
|
icon: Globe
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: false,
|
|
|
|
|
|
title: '仅指定部门',
|
|
|
|
|
|
description: '只有选中的部门成员可以访问这个知识库',
|
|
|
|
|
|
icon: Building2
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-01-20 02:14:51 +08:00
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
modelValue: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
is_shared: true,
|
|
|
|
|
|
accessible_department_ids: []
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否自动选中当前用户所在部门
|
|
|
|
|
|
autoSelectUserDept: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
|
|
|
|
|
|
// 本地状态,直接从 props 初始化
|
|
|
|
|
|
const config = reactive({
|
|
|
|
|
|
is_shared: true,
|
|
|
|
|
|
accessible_department_ids: []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 config
|
|
|
|
|
|
const initConfig = () => {
|
|
|
|
|
|
// 后端返回的是 accessible_departments,前端使用 accessible_department_ids
|
2026-01-22 00:28:43 +08:00
|
|
|
|
const sourceDepts =
|
|
|
|
|
|
props.modelValue.accessible_department_ids ?? props.modelValue.accessible_departments ?? []
|
2026-01-20 02:14:51 +08:00
|
|
|
|
config.is_shared = props.modelValue.is_shared ?? true
|
2026-01-22 00:28:43 +08:00
|
|
|
|
config.accessible_department_ids = sourceDepts.map((id) => Number(id))
|
2026-01-20 02:14:51 +08:00
|
|
|
|
console.log('[ShareConfigForm] initConfig:', JSON.stringify(config))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只在组件挂载时初始化一次
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
initConfig()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 监听本地 config 变化,同步到父组件
|
2026-01-22 00:28:43 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
config,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
console.log('[ShareConfigForm] config 变化,emit:', JSON.stringify(newVal))
|
|
|
|
|
|
emit('update:modelValue', {
|
|
|
|
|
|
is_shared: newVal.is_shared,
|
|
|
|
|
|
accessible_department_ids: newVal.accessible_department_ids
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
2026-01-20 02:14:51 +08:00
|
|
|
|
|
2026-04-28 22:02:15 +08:00
|
|
|
|
const setShareMode = (isShared) => {
|
|
|
|
|
|
if (config.is_shared === isShared) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
config.is_shared = isShared
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 02:14:51 +08:00
|
|
|
|
// 监听共享模式变化
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => config.is_shared,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (!newVal && props.autoSelectUserDept && config.accessible_department_ids.length === 0) {
|
|
|
|
|
|
// 切换到指定部门模式且未选中任何部门时,默认选中当前用户所在部门
|
|
|
|
|
|
tryAutoSelectUserDept()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试自动选中用户所在部门
|
|
|
|
|
|
const tryAutoSelectUserDept = () => {
|
|
|
|
|
|
const userDeptId = userStore.departmentId
|
|
|
|
|
|
if (userDeptId) {
|
|
|
|
|
|
const deptExists = departments.value.find((d) => d.id === userDeptId)
|
|
|
|
|
|
if (deptExists) {
|
|
|
|
|
|
// 确保存储为数字类型(a-select 返回的是字符串)
|
|
|
|
|
|
config.accessible_department_ids = [Number(userDeptId)]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听用户部门变化(处理时序问题:departmentId 可能在组件 mounted 后才获取到)
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => userStore.departmentId,
|
|
|
|
|
|
(newDeptId) => {
|
2026-01-22 00:28:43 +08:00
|
|
|
|
if (
|
|
|
|
|
|
props.autoSelectUserDept &&
|
|
|
|
|
|
!config.is_shared &&
|
|
|
|
|
|
config.accessible_department_ids.length === 0 &&
|
|
|
|
|
|
newDeptId
|
|
|
|
|
|
) {
|
2026-01-20 02:14:51 +08:00
|
|
|
|
tryAutoSelectUserDept()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 部门选项
|
|
|
|
|
|
const departmentOptions = computed(() =>
|
|
|
|
|
|
departments.value.map((dept) => ({
|
|
|
|
|
|
label: dept.name,
|
|
|
|
|
|
value: Number(dept.id)
|
|
|
|
|
|
}))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 加载部门列表
|
|
|
|
|
|
const loadDepartments = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await departmentApi.getDepartments()
|
|
|
|
|
|
departments.value = res.departments || res || []
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要,自动选中用户所在部门
|
2026-01-22 00:28:43 +08:00
|
|
|
|
if (
|
|
|
|
|
|
props.autoSelectUserDept &&
|
|
|
|
|
|
!config.is_shared &&
|
|
|
|
|
|
config.accessible_department_ids.length === 0
|
|
|
|
|
|
) {
|
2026-01-20 02:14:51 +08:00
|
|
|
|
tryAutoSelectUserDept()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载部门列表失败:', e)
|
|
|
|
|
|
departments.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadDepartments()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 验证当前用户所在部门是否在可访问范围内
|
|
|
|
|
|
// 返回 { valid: boolean, message: string }
|
|
|
|
|
|
const validate = () => {
|
|
|
|
|
|
// 全员共享模式不需要验证
|
|
|
|
|
|
if (config.is_shared) {
|
|
|
|
|
|
return { valid: true, message: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 指定部门模式,需要验证当前用户所在部门是否在列表中
|
|
|
|
|
|
const userDeptId = userStore.departmentId
|
|
|
|
|
|
if (!userDeptId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
valid: false,
|
|
|
|
|
|
message: '您不属于任何部门,无法使用指定部门共享模式'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!config.accessible_department_ids.includes(userDeptId)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
valid: false,
|
|
|
|
|
|
message: '您所在的部门必须在可访问部门范围内'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { valid: true, message: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 暴露方法给父组件
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
config,
|
|
|
|
|
|
validate
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.share-config-form {
|
|
|
|
|
|
.share-config-content {
|
2026-04-28 22:02:15 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
.share-mode-cards {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.share-mode-card {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
min-height: 76px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
2026-05-01 22:17:27 +08:00
|
|
|
|
transition:
|
|
|
|
|
|
border-color 0.2s ease,
|
|
|
|
|
|
background-color 0.2s ease,
|
|
|
|
|
|
box-shadow 0.2s ease;
|
2026-04-28 22:02:15 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
|
&:focus-visible {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:focus-visible {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: 0 0 0 2px var(--main-20);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
background: var(--main-10);
|
|
|
|
|
|
box-shadow: 0 0 0 1px var(--main-20);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-icon-wrapper {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: var(--gray-50);
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-icon {
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
transition: color 0.2s ease;
|
|
|
|
|
|
}
|
2026-01-20 02:14:51 +08:00
|
|
|
|
|
2026-04-28 22:02:15 +08:00
|
|
|
|
.card-content {
|
|
|
|
|
|
min-width: 0;
|
2026-01-20 02:14:51 +08:00
|
|
|
|
display: flex;
|
2026-04-28 22:02:15 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
line-height: 1.35;
|
2026-01-20 02:14:51 +08:00
|
|
|
|
}
|
2026-04-28 22:02:15 +08:00
|
|
|
|
|
|
|
|
|
|
&.active .card-icon-wrapper {
|
|
|
|
|
|
background: var(--main-0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active .card-icon {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-description {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dept-selection {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compact-dept-selection {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border: 1px solid var(--gray-150);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: linear-gradient(180deg, var(--gray-0) 0%, var(--gray-50) 100%);
|
2026-01-20 02:14:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 22:02:15 +08:00
|
|
|
|
.dept-selection-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dept-selection-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dept-selection-hint {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.5;
|
2026-01-20 02:14:51 +08:00
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 22:02:15 +08:00
|
|
|
|
.dept-selection-input {
|
|
|
|
|
|
:deep(.ant-select-selector) {
|
|
|
|
|
|
min-height: 36px;
|
|
|
|
|
|
padding: 3px 8px !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
border-color: var(--gray-200) !important;
|
|
|
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:deep(.ant-select-focused .ant-select-selector),
|
|
|
|
|
|
&:deep(.ant-select-selector:hover) {
|
|
|
|
|
|
border-color: var(--main-color) !important;
|
|
|
|
|
|
}
|
2026-01-20 02:14:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|