feat(web): 创作者审核页支持开号/重置密码;登录落地按角色分流 + 首登强制改密

为什么:后台能开号了,但界面还没有入口;而且创作者登录后会落到 /dashboard/index(他没有运营后台权限)
直接掉 404。这批把开号入口、一次性凭据展示、落地分流与首登改密一次补齐。

- 创作者入驻页:「新增创作者」按钮(名称/类型/联系人/邮箱/国家)、「账号」列(已开通/未开通)、
  行内「开通账号 / 重置密码」;密码一次性弹窗展示(等宽可选中 + 一键复制 + 提示只显示这一次)
- 落地分流:新增 landingPath()(取账号能进的第一个菜单),登录与访问根路径都走它 ——
  创作者落 /creator-center/workspace,超管第一条仍是工作台(行为不变)
- 首登强制改密:Layout 挂 MustChangePasswordDialog,登录或刷新后若 profile 带 mustChangePassword 就弹不可关闭的
  改密框,改完清标记;只能改密或退出登录
This commit is contained in:
Vicky committed 2026-10-15 16:01:13 +08:00
1 parent bac070b60b
commit 8ce43ccd4c
10 files changed
+378 -7

No files matched your search

+30
View File
@@ -62,6 +62,36 @@ export const auditCreatorApi = (id: number, data: Record<string, unknown>) => {
return request.put<CreatorItem>({ url: `/admin/creators/${id}/audit`, data })
}
/** 手工录入创作者主体(C 端申请入驻未实现前的入口) */
export const createCreatorApi = (data: Record<string, unknown>) => {
return request.post<number>({ 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<CreatorAccountResult>({
url: `/admin/creators/${id}/account`,
data: { username }
})
}
/** 重置创作者账号密码(一次性明文返回) */
export const resetCreatorPasswordApi = (id: number) => {
return request.post<CreatorAccountResult>({
url: `/admin/creators/${id}/account/reset-password`
})
}
export const pageSubmissionsApi = (params: Record<string, unknown>) => {
return request.get<PageResult<SubmissionItem>>({ url: '/admin/submissions/page', params })
}
+5
View File
@@ -27,3 +27,8 @@ export const getAdminRoleApi = (
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/mock/role/list2', params })
}
/** 自助改密(首登强制改密也走这个接口) */
export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => {
return request.post<void>({ url: '/admin/auth/password', data })
}
+3
View File
@@ -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({
<Backtop></Backtop>
<MustChangePasswordDialog></MustChangePasswordDialog>
<Setting></Setting>
</section>
)
+10
View File
@@ -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 }
+13
View File
@@ -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,
+13
View File
@@ -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'
}
+3 -2
View File
@@ -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() })
}
}
@@ -0,0 +1,80 @@
<script setup>
import { computed, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { changePasswordApi } from '@/api/login'
import { useUserStore } from '@/store/modules/user'
/**
* 首登强制改密:后台开号 / 重置密码时置的标记,登录(或刷新)后必须先把密码改掉。
* 对话框不可关闭,只能改密或退出登录 —— 初始密码是运营转达的,越早换掉越好。
*/
const userStore = useUserStore()
const visible = computed(() => Boolean(userStore.getUserInfo?.mustChangePassword))
const form = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
const saving = ref(false)
async function submit() {
if (!form.oldPassword) return ElMessage.warning('请输入当前密码(运营给你的初始密码)')
if (!form.newPassword || form.newPassword.length < 8) return ElMessage.warning('新密码至少 8 位')
if (form.newPassword !== form.confirmPassword) return ElMessage.warning('两次输入的新密码不一致')
saving.value = true
try {
await changePasswordApi({
oldPassword: form.oldPassword,
newPassword: form.newPassword
})
ElMessage.success('密码已更新')
userStore.setUserInfo({ ...userStore.getUserInfo, mustChangePassword: false })
form.oldPassword = ''
form.newPassword = ''
form.confirmPassword = ''
} finally {
saving.value = false
}
}
async function logout() {
await userStore.logout()
window.location.reload()
}
</script>
<template>
<el-dialog
:model-value="visible"
title="首次登录请修改密码"
width="460px"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-alert
class="must-change-alert"
type="warning"
:closable="false"
show-icon
title="当前使用的是运营转达的初始密码,请改成只有你知道的密码后再开始使用"
/>
<el-form label-position="top">
<el-form-item label="当前密码(初始密码)" required>
<el-input v-model="form.oldPassword" type="password" show-password autocomplete="off" />
</el-form-item>
<el-form-item label="新密码(至少 8 位)" required>
<el-input v-model="form.newPassword" type="password" show-password autocomplete="off" />
</el-form-item>
<el-form-item label="确认新密码" required>
<el-input v-model="form.confirmPassword" type="password" show-password autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="logout">退出登录</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存新密码</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.must-change-alert {
margin-bottom: 14px;
}
</style>
+109 -3
View File
@@ -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()
</script>
@@ -47,7 +61,9 @@ const {
eyebrow="创作者管理"
title="创作者入驻"
description="审核创作者主体与资质,通过后创作者才能提交内容。"
/>
>
<el-button v-if="canAudit" type="primary" @click="openCreate">新增创作者</el-button>
</PageHeading>
<section class="panel">
<div class="console-toolbar">
@@ -127,6 +143,12 @@ const {
</el-tag>
</template>
</el-table-column>
<el-table-column label="账号" width="110">
<template #default="{ row }">
<el-tag v-if="row.adminUserId" type="success" effect="light">已开通</el-tag>
<span v-else class="console-muted">未开通</span>
</template>
</el-table-column>
<el-table-column
prop="auditAt"
label="审核时间"
@@ -141,7 +163,7 @@ const {
show-overflow-tooltip
:formatter="dateCell"
/>
<el-table-column label="操作" width="230" fixed="right">
<el-table-column label="操作" width="330" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button type="primary" size="small" @click="openDocuments(row)">资质</el-button>
@@ -155,6 +177,21 @@ const {
@click="freeze(row)"
>冻结</el-button
>
<el-button
v-if="canOpenAccount && !row.adminUserId"
type="primary"
size="small"
:loading="accountSaving"
:disabled="row.status !== 'approved'"
@click="openAccount(row)"
>开通账号</el-button
>
<el-button
v-if="canOpenAccount && row.adminUserId"
size="small"
@click="resetPassword(row)"
>重置密码</el-button
>
</div>
</template>
</el-table-column>
@@ -231,4 +268,73 @@ const {
<el-button type="primary" :loading="auditSaving" @click="confirmAudit">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="createVisible" title="新增创作者" width="480px">
<el-form label-position="top">
<el-form-item label="创作者名称" required>
<el-input v-model="createForm.name" maxlength="128" placeholder="公司名或个人名" />
</el-form-item>
<el-form-item label="类型">
<el-radio-group v-model="createForm.type">
<el-radio v-for="item in CREATOR_TYPE" :key="item.value" :value="item.value">{{
item.label
}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="createForm.contactName" maxlength="64" />
</el-form-item>
<el-form-item label="联系邮箱">
<el-input v-model="createForm.contactEmail" maxlength="128" />
</el-form-item>
<el-form-item label="国家 / 地区">
<el-input v-model="createForm.countryCode" maxlength="8" placeholder="ID" />
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
title="编号自动生成(CR+日期+序号),也是开通账号时默认的登录名;录入后需要先通过入驻审核才能开通账号"
/>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate">录入</el-button>
</template>
</el-dialog>
<el-dialog
:model-value="accountVisible"
:title="accountResult?.created ? '账号已开通' : '账号凭据'"
width="520px"
@close="closeAccount"
@update:model-value="closeAccount"
>
<el-alert
v-if="accountResult?.password"
class="creator-account-alert"
type="warning"
:closable="false"
show-icon
title="密码只显示这一次,关闭后无法再次查看;请立即复制并转达给创作者"
/>
<el-descriptions :column="1" border>
<el-descriptions-item label="登录账号">
<span class="creator-account-value">{{ accountResult?.username }}</span>
</el-descriptions-item>
<el-descriptions-item label="初始密码">
<span v-if="accountResult?.password" class="creator-account-value">{{
accountResult.password
}}</span>
<span v-else class="console-muted">本次未生成新密码(账号此前已开通)</span>
</el-descriptions-item>
<el-descriptions-item label="说明">
<span class="console-muted">{{ accountResult?.message }}</span>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button v-if="accountResult?.password" @click="copyCredential">复制账号与密码</el-button>
<el-button type="primary" @click="closeAccount">我已保存</el-button>
</template>
</el-dialog>
</template>
@@ -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
}
}