From 8ce43ccd4c33d7233b29544a06a67f32d9fe8ea6 Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Thu, 15 Oct 2026 16:01:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=88=9B=E4=BD=9C=E8=80=85?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E9=A1=B5=E6=94=AF=E6=8C=81=E5=BC=80=E5=8F=B7?= =?UTF-8?q?/=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=EF=BC=9B=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=90=BD=E5=9C=B0=E6=8C=89=E8=A7=92=E8=89=B2=E5=88=86?= =?UTF-8?q?=E6=B5=81=20+=20=E9=A6=96=E7=99=BB=E5=BC=BA=E5=88=B6=E6=94=B9?= =?UTF-8?q?=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么:后台能开号了,但界面还没有入口;而且创作者登录后会落到 /dashboard/index(他没有运营后台权限) 直接掉 404。这批把开号入口、一次性凭据展示、落地分流与首登改密一次补齐。 - 创作者入驻页:「新增创作者」按钮(名称/类型/联系人/邮箱/国家)、「账号」列(已开通/未开通)、 行内「开通账号 / 重置密码」;密码一次性弹窗展示(等宽可选中 + 一键复制 + 提示只显示这一次) - 落地分流:新增 landingPath()(取账号能进的第一个菜单),登录与访问根路径都走它 —— 创作者落 /creator-center/workspace,超管第一条仍是工作台(行为不变) - 首登强制改密:Layout 挂 MustChangePasswordDialog,登录或刷新后若 profile 带 mustChangePassword 就弹不可关闭的 改密框,改完清标记;只能改密或退出登录 --- src/api/admin/creator.ts | 30 +++++ src/api/login/index.ts | 5 + src/layout/Layout.vue | 3 + src/permission.ts | 10 ++ src/styles/business/creator-center.css | 13 ++ src/utils/landing.ts | 13 ++ src/views/Login/components/LoginForm.vue | 5 +- .../components/MustChangePasswordDialog.vue | 80 ++++++++++++ src/views/creator/CreatorAuditView.vue | 112 ++++++++++++++++- .../creator/composables/useCreatorAudit.js | 114 +++++++++++++++++- 10 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 src/utils/landing.ts create mode 100644 src/views/components/MustChangePasswordDialog.vue diff --git a/src/api/admin/creator.ts b/src/api/admin/creator.ts index 762f314..275571d 100644 --- a/src/api/admin/creator.ts +++ b/src/api/admin/creator.ts @@ -62,6 +62,36 @@ export const auditCreatorApi = (id: number, data: Record) => { return request.put({ url: `/admin/creators/${id}/audit`, data }) } +/** 手工录入创作者主体(C 端申请入驻未实现前的入口) */ +export const createCreatorApi = (data: Record) => { + return request.post({ url: '/admin/creators', data }) +} + +export interface CreatorAccountResult { + creatorId: number + adminUserId: number + username: string + /** 一次性明文密码:只在开通 / 重置的这次响应里出现,之后拿不回来 */ + password: string | null + created: boolean + message: string +} + +/** 开通创作者账号(仅审核通过可开;已开通则幂等返回) */ +export const openCreatorAccountApi = (id: number, username?: string) => { + return request.post({ + url: `/admin/creators/${id}/account`, + data: { username } + }) +} + +/** 重置创作者账号密码(一次性明文返回) */ +export const resetCreatorPasswordApi = (id: number) => { + return request.post({ + url: `/admin/creators/${id}/account/reset-password` + }) +} + export const pageSubmissionsApi = (params: Record) => { return request.get>({ url: '/admin/submissions/page', params }) } diff --git a/src/api/login/index.ts b/src/api/login/index.ts index 072ef09..3d2495f 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -27,3 +27,8 @@ export const getAdminRoleApi = ( export const getTestRoleApi = (params: RoleParams): Promise> => { return request.get({ url: '/mock/role/list2', params }) } + +/** 自助改密(首登强制改密也走这个接口) */ +export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => { + return request.post({ url: '/admin/auth/password', data }) +} diff --git a/src/layout/Layout.vue b/src/layout/Layout.vue index 485a778..8238ac5 100644 --- a/src/layout/Layout.vue +++ b/src/layout/Layout.vue @@ -3,6 +3,7 @@ import { computed, defineComponent, unref } from 'vue' import { useAppStore } from '@/store/modules/app' import { Backtop } from '@/components/Backtop' import { Setting } from '@/components/Setting' +import MustChangePasswordDialog from '@/views/components/MustChangePasswordDialog.vue' import { useRenderLayout } from './components/useRenderLayout' import { useDesign } from '@/hooks/web/useDesign' @@ -59,6 +60,8 @@ export default defineComponent({ + + ) diff --git a/src/permission.ts b/src/permission.ts index 756b65a..3991b8d 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -8,6 +8,7 @@ import { usePageLoading } from '@/hooks/web/usePageLoading' import { NO_REDIRECT_WHITE_LIST } from '@/constants' import { useUserStoreWithOut } from '@/store/modules/user' import { getAdminRoleApi, getProfileApi } from '@/api/login' +import { landingPath } from '@/utils/landing' const { start, done } = useNProgress() @@ -63,6 +64,15 @@ router.beforeEach(async (to, from, next) => { permissionStore.getAddRouters.forEach((route) => { router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 }) + + // 根路径按"这个账号能进的第一个菜单"落地:路由表里写死的 redirect 是 /dashboard/index, + // 创作者没有运营后台权限,访问 '/' 会直接掉 404 + if (to.path === '/') { + permissionStore.setIsAddRouters(true) + next({ path: landingPath(), replace: true }) + return + } + const redirectPath = from.query.redirect || to.path const redirect = decodeURIComponent(redirectPath as string) const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect } diff --git a/src/styles/business/creator-center.css b/src/styles/business/creator-center.css index 11ceb3c..0f0b3af 100644 --- a/src/styles/business/creator-center.css +++ b/src/styles/business/creator-center.css @@ -270,6 +270,19 @@ html.dark .creator-guide-title { color: var(--el-text-color-primary); } +/* 开通账号后展示的一次性凭据:等宽、可选中复制 */ +.creator-account-value { + font-family: Menlo, Consolas, monospace; + font-size: 14px; + font-weight: 600; + letter-spacing: 0; + user-select: all; +} + +.creator-account-alert { + margin-bottom: 14px; +} + html.dark .creator-profile-name, html.dark .creator-profile-meta b, html.dark .creator-drama-title, diff --git a/src/utils/landing.ts b/src/utils/landing.ts new file mode 100644 index 0000000..479bc51 --- /dev/null +++ b/src/utils/landing.ts @@ -0,0 +1,13 @@ +import { usePermissionStoreWithOut } from '@/store/modules/permission' + +/** + * 登录 / 访问根路径时的落地页:取"这个账号实际能进的第一个菜单"。 + * + * 原来写死 `/dashboard/index`,创作者没有运营后台权限,登录后会直接掉进 404; + * 现在按账号自己的路由表决定 —— 创作者落创作者中心,超管第一条仍是工作台(行为不变)。 + */ +export function landingPath(): string { + const permissionStore = usePermissionStoreWithOut() + const first = permissionStore.addRouters?.[0]?.path + return (first as string) || '/dashboard/index' +} diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index eb48adb..007d2b2 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -7,6 +7,7 @@ import { useForm } from '@/hooks/web/useForm' import { loginApi, getTestRoleApi, getAdminRoleApi } from '@/api/login' import { useAppStore } from '@/store/modules/app' import { usePermissionStore } from '@/store/modules/permission' +import { landingPath } from '@/utils/landing' import { useRouter } from 'vue-router' import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router' import { UserType } from '@/api/login/types' @@ -200,7 +201,7 @@ const signIn = async () => { addRoute(route as RouteRecordRaw) // 动态添加可访问路由表 }) permissionStore.setIsAddRouters(true) - push({ path: redirect.value || permissionStore.addRouters[0].path }) + push({ path: redirect.value || landingPath() }) } } } finally { @@ -231,7 +232,7 @@ const getRole = async () => { addRoute(route as RouteRecordRaw) // 动态添加可访问路由表 }) permissionStore.setIsAddRouters(true) - push({ path: redirect.value || permissionStore.addRouters[0].path }) + push({ path: redirect.value || landingPath() }) } } diff --git a/src/views/components/MustChangePasswordDialog.vue b/src/views/components/MustChangePasswordDialog.vue new file mode 100644 index 0000000..1e9d702 --- /dev/null +++ b/src/views/components/MustChangePasswordDialog.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/views/creator/CreatorAuditView.vue b/src/views/creator/CreatorAuditView.vue index eca3772..d83dfbf 100644 --- a/src/views/creator/CreatorAuditView.vue +++ b/src/views/creator/CreatorAuditView.vue @@ -13,6 +13,8 @@ import { const { can } = useRbac() const canAudit = can('creator:audit') +/** 开通 / 重置账号是独立权限点(第一阶段只给超管) */ +const canOpenAccount = can('creator:account') const { rows, @@ -38,7 +40,19 @@ const { openDocuments, openAudit, confirmAudit, - freeze + freeze, + createVisible, + createSaving, + createForm, + openCreate, + confirmCreate, + accountVisible, + accountSaving, + accountResult, + openAccount, + resetPassword, + copyCredential, + closeAccount } = useCreatorAudit() @@ -47,7 +61,9 @@ const { eyebrow="创作者管理" title="创作者入驻" description="审核创作者主体与资质,通过后创作者才能提交内容。" - /> + > + 新增创作者 +
@@ -127,6 +143,12 @@ const { + + + - + @@ -231,4 +268,73 @@ const { 提交 + + + + + + + + + {{ + item.label + }} + + + + + + + + + + + + + + + + + + diff --git a/src/views/creator/composables/useCreatorAudit.js b/src/views/creator/composables/useCreatorAudit.js index 41522c1..be7d9ca 100644 --- a/src/views/creator/composables/useCreatorAudit.js +++ b/src/views/creator/composables/useCreatorAudit.js @@ -1,6 +1,13 @@ import { onMounted, reactive, ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' -import { auditCreatorApi, listCreatorDocumentsApi, pageCreatorsApi } from '@/api/admin/creator' +import { + auditCreatorApi, + createCreatorApi, + listCreatorDocumentsApi, + openCreatorAccountApi, + pageCreatorsApi, + resetCreatorPasswordApi +} from '@/api/admin/creator' /** 与后端 CreatorService 的状态取值保持一致 */ export const CREATOR_STATUS = [ @@ -138,6 +145,97 @@ export function useCreatorAudit() { onMounted(load) + // ---------- 手工录入创作者主体 ---------- + const createVisible = ref(false) + const createSaving = ref(false) + const createForm = reactive({ + name: '', + type: 'COMPANY', + contactName: '', + contactEmail: '', + countryCode: 'ID' + }) + + function openCreate() { + Object.assign(createForm, { + name: '', + type: 'COMPANY', + contactName: '', + contactEmail: '', + countryCode: 'ID' + }) + createVisible.value = true + } + + async function confirmCreate() { + if (!createForm.name.trim()) return ElMessage.warning('请填写创作者名称') + createSaving.value = true + try { + await createCreatorApi({ ...createForm, name: createForm.name.trim() }) + ElMessage.success('已录入,待入驻审核通过后才能开通账号') + createVisible.value = false + await load() + } finally { + createSaving.value = false + } + } + + // ---------- 开通账号 / 重置密码 ---------- + const accountVisible = ref(false) + const accountSaving = ref(false) + /** 一次性凭据展示:password 只在开通 / 重置那一次有值 */ + const accountResult = ref(null) + + async function openAccount(row) { + accountSaving.value = true + try { + const res = await openCreatorAccountApi(row.id) + accountResult.value = res.data || null + accountVisible.value = true + await load() + } finally { + accountSaving.value = false + } + } + + async function resetPassword(row) { + try { + await ElMessageBox.confirm( + `重置后「${row.name}」的旧密码立即失效,需要把新密码重新转达给他。确认重置?`, + '重置账号密码', + { confirmButtonText: '确认重置', cancelButtonText: '取消', type: 'warning' } + ) + } catch { + return + } + accountSaving.value = true + try { + const res = await resetCreatorPasswordApi(row.id) + accountResult.value = res.data || null + accountVisible.value = true + } finally { + accountSaving.value = false + } + } + + /** 一次性密码要能被复制走;复制完由运营自己销毁截图 */ + async function copyCredential() { + const item = accountResult.value + if (!item?.password) return + const text = `登录地址:${window.location.origin}/#/login\n账号:${item.username}\n初始密码:${item.password}` + try { + await navigator.clipboard.writeText(text) + ElMessage.success('账号与密码已复制') + } catch { + ElMessage.warning('浏览器不允许自动复制,请手动选中复制') + } + } + + function closeAccount() { + accountVisible.value = false + accountResult.value = null + } + return { rows, total, @@ -162,6 +260,18 @@ export function useCreatorAudit() { openDocuments, openAudit, confirmAudit, - freeze + freeze, + createVisible, + createSaving, + createForm, + openCreate, + confirmCreate, + accountVisible, + accountSaving, + accountResult, + openAccount, + resetPassword, + copyCredential, + closeAccount } }