init local code
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { schoolApi, siteApi, themeApi, portalBase } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { SchoolCard, SchoolOption, ThemeVo } from "@/types/cms";
|
||||
import ThemePicker from "@/components/theme/ThemePicker.vue";
|
||||
|
||||
/**
|
||||
* 学校工作台(一级页,docs/09 §3.3 方案A):全屏无壳。
|
||||
* 学校卡片网格(校名/站点构成/文章规模/进入管理/门户预览);
|
||||
* 超管:+添加学校(UserBase 学校选择器绑定 + 初始化主站,复用 /api/site/save)
|
||||
* 与「全部站点总览」入口;校级单校用户自动直进本校管理壳。
|
||||
*/
|
||||
const router = useRouter();
|
||||
const store = useCmsStore();
|
||||
|
||||
const cards = ref<SchoolCard[]>([]);
|
||||
const loading = ref(false);
|
||||
const themes = ref<ThemeVo[]>([]);
|
||||
|
||||
const isSuper = computed(() => store.isSuperAdmin);
|
||||
const userInfo = computed(() => ({
|
||||
name: store.session.baseUserName || "管理员",
|
||||
avatar: store.session.baseUserAvatar,
|
||||
}));
|
||||
|
||||
async function run() {
|
||||
loading.value = true;
|
||||
try {
|
||||
cards.value = (await schoolApi.list().send()) || [];
|
||||
maybeAutoEnter();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 单校校级用户跳过工作台直达本校管理壳;超管常驻工作台(多校管理诉求) */
|
||||
function maybeAutoEnter() {
|
||||
if (store.isSuperAdmin) return;
|
||||
if (cards.value.length === 1) {
|
||||
enter(cards.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function enter(card: SchoolCard) {
|
||||
store.enterSchool(
|
||||
{
|
||||
schId: card.schId,
|
||||
schName: card.schName,
|
||||
mainSiteId: card.mainSiteId,
|
||||
mainSiteCode: card.mainSiteCode,
|
||||
mainSiteName: card.mainSiteName,
|
||||
},
|
||||
(store.user?.sites || []).filter(
|
||||
(s: any) => s.id === card.mainSiteId || s.parentSiteId === card.mainSiteId
|
||||
)
|
||||
);
|
||||
router.push(`/sch/${card.mainSiteCode}/article`);
|
||||
}
|
||||
|
||||
function openPortal(card: SchoolCard) {
|
||||
window.open(`${portalBase(card.mainSiteCode)}/`, "_blank");
|
||||
}
|
||||
|
||||
/** 超管全局站点视图(借道当前/第一个学校上下文 + scope=all) */
|
||||
function goAllSites() {
|
||||
const code = store.schoolCode || cards.value[0]?.mainSiteCode;
|
||||
if (code) {
|
||||
router.push({ path: `/sch/${code}/site`, query: { scope: "all" } });
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 添加学校(超管):绑定已有学校 + 初始化主站 ===== */
|
||||
const modalVisible = ref(false);
|
||||
const saving = ref(false);
|
||||
const options = ref<SchoolOption[]>([]);
|
||||
const optionsLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const form = ref({
|
||||
schId: undefined as number | undefined,
|
||||
schName: "",
|
||||
code: "",
|
||||
name: "",
|
||||
theme: "default",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
schId: [{ required: true, message: "请选择学校(来自用户中心)", trigger: "change" }],
|
||||
code: [
|
||||
{ required: true, message: "主站编码不得为空", trigger: "blur" },
|
||||
{ pattern: /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/, message: "仅限小写字母/数字/短横线", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "主站名称不得为空", trigger: "blur" }],
|
||||
};
|
||||
|
||||
function openAdd() {
|
||||
modalVisible.value = true;
|
||||
form.value = { schId: undefined, schName: "", code: "", name: "", theme: "default" };
|
||||
loadOptions();
|
||||
if (!themes.value.length) loadThemes();
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
optionsLoading.value = true;
|
||||
try {
|
||||
options.value = (await schoolApi.options().send()) || [];
|
||||
if (!options.value.length) {
|
||||
Message.warning("未获取到学校列表(用户中心不可达或暂无学校数据)");
|
||||
}
|
||||
} finally {
|
||||
optionsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadThemes() {
|
||||
try {
|
||||
themes.value = (await themeApi.list().send()) || [];
|
||||
} catch {
|
||||
themes.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onSchoolPick(id: number) {
|
||||
const opt = options.value.find(o => o.id === id);
|
||||
form.value.schName = opt?.name || "";
|
||||
if (!form.value.name && opt) {
|
||||
form.value.name = `${opt.name}门户网站`;
|
||||
}
|
||||
if (!form.value.code && opt) {
|
||||
form.value.code = `sch-${opt.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await siteApi
|
||||
.save({
|
||||
code: form.value.code,
|
||||
name: form.value.name,
|
||||
schId: form.value.schId,
|
||||
schName: form.value.schName || undefined,
|
||||
theme: form.value.theme || undefined,
|
||||
parentSiteId: 0,
|
||||
status: 1,
|
||||
})
|
||||
.send();
|
||||
Message.success("学校主站创建成功,可在卡片「进入管理」开始配置内容!");
|
||||
modalVisible.value = false;
|
||||
run();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const linkUserInfo = () => {
|
||||
window.location.href = config.baseHost + "/v/userinfo";
|
||||
};
|
||||
const logout = () => {
|
||||
const url = "/cas/logout?service=" + window.location.href;
|
||||
window.location.href = config.ssoHost + url;
|
||||
};
|
||||
|
||||
onMounted(run);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workbench">
|
||||
<header class="wb-header fix">
|
||||
<h4 class="fl align-middle">云海CMS · 学校工作台</h4>
|
||||
<div class="wb-header-aside fr">
|
||||
<Button v-if="isSuper" size="small" class="wb-btn" @click="goAllSites" :disabled="!cards.length">
|
||||
<Icon type="ios-globe" />
|
||||
全部站点总览
|
||||
</Button>
|
||||
<Button v-if="isSuper" size="small" type="primary" class="wb-btn" @click="openAdd">
|
||||
<Icon type="ios-add" />
|
||||
添加学校
|
||||
</Button>
|
||||
<span class="delimiter align-middle"></span>
|
||||
<img :src="userInfo.avatar" v-if="userInfo.avatar" class="fl align-middle wb-avatar" @click="linkUserInfo" />
|
||||
<a class="name fl align-middle" href="javascript:void(0);" @click="linkUserInfo">{{ userInfo.name }}</a>
|
||||
<span class="delimiter align-middle"></span>
|
||||
<a href="javascript:;" @click="logout" title="退出登陆">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wb-body">
|
||||
<div class="wb-title">
|
||||
<h3>我的学校</h3>
|
||||
<p class="wb-sub">选择学校进入内容管理;学校与站点由平台管理员在「添加学校」中绑定创建。</p>
|
||||
</div>
|
||||
|
||||
<Spin v-if="loading" size="large" fix />
|
||||
<template v-else>
|
||||
<div v-if="cards.length" class="card-grid">
|
||||
<div v-for="card in cards" :key="card.mainSiteId" class="school-card" :class="{ disabled: card.status !== 1 }">
|
||||
<div class="card-head">
|
||||
<Icon type="ios-school" :size="26" />
|
||||
<div class="card-name">
|
||||
<h4>{{ card.schName || card.mainSiteName }}</h4>
|
||||
<p>{{ card.mainSiteName }}({{ card.mainSiteCode }})</p>
|
||||
</div>
|
||||
<Tag v-if="card.status !== 1" color="default" class="fr">停用</Tag>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<span>{{ card.siteCount > 1 ? `主站 + ${card.siteCount - 1} 个院系子站` : "仅主站" }}</span>
|
||||
<span>文章 {{ card.articleCount }} 篇</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<Button type="primary" long :disabled="card.status !== 1" @click="enter(card)">进入管理</Button>
|
||||
<Button long class="portal-btn" @click="openPortal(card)">
|
||||
<Icon type="ios-eye" />
|
||||
门户预览
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<Icon type="ios-school" :size="48" />
|
||||
<p v-if="isSuper">还没有学校 —— 点击右上角「添加学校」绑定用户中心学校并创建门户主站。</p>
|
||||
<p v-else>暂无可管理的学校站点,请联系平台管理员。</p>
|
||||
<Button v-if="isSuper" type="primary" @click="openAdd">
|
||||
<Icon type="ios-add" />
|
||||
添加学校
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 添加学校弹窗:学校选择器(UserBase oapi)+ 主站信息 -->
|
||||
<Modal v-model="modalVisible" title="添加学校" :mask-closable="false" width="640px">
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="110">
|
||||
<FormItem label="选择学校" prop="schId">
|
||||
<Select
|
||||
v-model="form.schId"
|
||||
filterable
|
||||
:loading="optionsLoading"
|
||||
placeholder="输入名称搜索用户中心学校"
|
||||
style="width: 100%"
|
||||
@on-change="onSchoolPick"
|
||||
>
|
||||
<Option v-for="opt in options" :key="opt.id" :value="opt.id">{{ opt.name }}</Option>
|
||||
</Select>
|
||||
<p class="form-tip">学校主数据来自用户中心(只绑定,不新建);校名自动带出,学校账号/组织仍在用户中心维护。</p>
|
||||
</FormItem>
|
||||
<FormItem label="主站编码" prop="code">
|
||||
<Input v-model="form.code" placeholder="URL 路径用,如 demo" />
|
||||
</FormItem>
|
||||
<FormItem label="主站名称" prop="name">
|
||||
<Input v-model="form.name" :maxlength="128" placeholder="如 XX学校门户网站" />
|
||||
</FormItem>
|
||||
<FormItem label="皮肤包">
|
||||
<ThemePicker :themes="themes" :model-value="form.theme" @update:model-value="(v: string) => (form.theme = v)" />
|
||||
<p class="form-tip">建好之后可在校内「站点管理」微调主题参数与站点配置。</p>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="create">创建学校主站</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.workbench {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.wb-header {
|
||||
height: 56px;
|
||||
background: #1f2a3a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
h4 {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
line-height: 56px;
|
||||
}
|
||||
.wb-header-aside {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.name {
|
||||
color: #fff;
|
||||
}
|
||||
.delimiter {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
display: inline-block;
|
||||
}
|
||||
.wb-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.wb-btn {
|
||||
// 深色 header 上按钮保持默认浅色块
|
||||
}
|
||||
}
|
||||
}
|
||||
.wb-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32px 48px;
|
||||
}
|
||||
.wb-title {
|
||||
margin-bottom: 24px;
|
||||
h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #17233d;
|
||||
}
|
||||
.wb-sub {
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
.school-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(31, 42, 58, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
color: #2d8cf0;
|
||||
.card-name {
|
||||
h4 {
|
||||
margin: 0 0 2px;
|
||||
color: #17233d;
|
||||
font-size: 16px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 14px 0 18px;
|
||||
color: #515a6e;
|
||||
font-size: 13px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.portal-btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.empty-state {
|
||||
max-width: 1200px;
|
||||
border: 1px dashed #dcdee2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 64px 0;
|
||||
text-align: center;
|
||||
color: #808695;
|
||||
p {
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
}
|
||||
.form-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user