chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)

为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。
下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与
api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动,
既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。

本提交只做归档,不改行为:
- 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理)
- 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口
- 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传)
- 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
Vicky committed 2026-10-15 13:28:01 +08:00
1 parent 40546bfd01
commit 44c2c46a68
59 files changed
+4082 -982

No files matched your search

@@ -0,0 +1,229 @@
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { getOperationLogApi, pageOperationLogsApi } from '@/api/admin/log'
import PageHeading from '@/views/components/PageHeading.vue'
const loading = ref(false)
const list = ref([])
const total = ref(0)
const filters = reactive({
username: '',
module: '',
status: '',
range: null
})
const pageIndex = ref(1)
const pageSize = ref(10)
const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const moduleOptions = [
{ value: '认证', label: '认证' },
{ value: '内容管理', label: '内容管理' },
{ value: '后台管理', label: '后台管理' }
]
async function load() {
loading.value = true
try {
const res = await pageOperationLogsApi({
pageIndex: pageIndex.value,
pageSize: pageSize.value,
username: filters.username.trim() || undefined,
module: filters.module || undefined,
status: filters.status || undefined,
startTime: filters.range?.[0] || undefined,
endTime: filters.range?.[1] || undefined
})
list.value = res.data?.list || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
function search() {
pageIndex.value = 1
load()
}
function resetFilters() {
filters.username = ''
filters.module = ''
filters.status = ''
filters.range = null
search()
}
function handleSizeChange() {
pageIndex.value = 1
load()
}
async function openDetail(row) {
detailVisible.value = true
detailLoading.value = true
detail.value = null
try {
const res = await getOperationLogApi(row.id)
detail.value = res.data
} finally {
detailLoading.value = false
}
}
/** 请求/响应体按 JSON 缩进显示,解析失败就原样展示 */
function pretty(text) {
if (!text) return ''
try {
return JSON.stringify(JSON.parse(text), null, 2)
} catch {
return text
}
}
onMounted(load)
</script>
<template>
<PageHeading
eyebrow="后台管理"
title="操作日志"
description="记录后台的写操作,包含操作人、时间、请求与响应内容。"
/>
<section class="panel">
<div class="library-toolbar">
<div class="drama-filters">
<el-input
v-model="filters.username"
:prefix-icon="Search"
clearable
placeholder="操作人"
@keyup.enter="search"
@clear="search"
/>
<el-select v-model="filters.module" clearable placeholder="全部模块" @change="search">
<el-option
v-for="item in moduleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="filters.status" clearable placeholder="全部结果" @change="search">
<el-option label="成功" value="SUCCESS" />
<el-option label="失败" value="FAIL" />
</el-select>
<el-date-picker
v-model="filters.range"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
@change="search"
/>
<el-button @click="resetFilters">重置</el-button>
</div>
<span class="table-result-count">共 {{ total }} 条</span>
</div>
<el-table
v-loading="loading"
:data="list"
row-key="id"
empty-text="暂无操作记录"
class="drama-el-table"
>
<el-table-column prop="createdAt" label="操作时间" width="170" />
<el-table-column label="操作人" width="140">
<template #default="{ row }">
{{ row.username || '(未登录)' }}
</template>
</el-table-column>
<el-table-column prop="module" label="模块" width="110" />
<el-table-column prop="action" label="操作" min-width="150" />
<el-table-column label="请求" min-width="260">
<template #default="{ row }">
<div class="episode-table-title">
<b>{{ row.requestMethod }} {{ row.requestUrl }}</b>
</div>
</template>
</el-table-column>
<el-table-column label="结果" width="90">
<template #default="{ row }">
<el-tag :type="row.status === 'SUCCESS' ? 'success' : 'danger'" effect="light">
{{ row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="耗时" width="90">
<template #default="{ row }">{{ row.durationMs }} ms</template>
</el-table-column>
<el-table-column prop="ip" label="IP" width="130" />
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="openDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pageIndex"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@current-change="load"
@size-change="handleSizeChange"
/>
</div>
</section>
<el-dialog v-model="detailVisible" title="操作详情" width="760px">
<div v-loading="detailLoading" class="log-detail">
<el-descriptions v-if="detail" :column="2" border>
<el-descriptions-item label="操作人">{{
detail.username || '(未登录)'
}}</el-descriptions-item>
<el-descriptions-item label="操作时间">{{ detail.createdAt }}</el-descriptions-item>
<el-descriptions-item label="模块">{{ detail.module }}</el-descriptions-item>
<el-descriptions-item label="操作">{{ detail.action }}</el-descriptions-item>
<el-descriptions-item label="请求方式">{{ detail.requestMethod }}</el-descriptions-item>
<el-descriptions-item label="接口地址">{{ detail.requestUrl }}</el-descriptions-item>
<el-descriptions-item label="结果">
<el-tag :type="detail.status === 'SUCCESS' ? 'success' : 'danger'" effect="light">
{{ detail.statusText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detail.durationMs }} ms</el-descriptions-item>
<el-descriptions-item label="IP">{{ detail.ip }}</el-descriptions-item>
<el-descriptions-item label="查询参数">{{
detail.requestParams || '—'
}}</el-descriptions-item>
</el-descriptions>
<template v-if="detail">
<div class="log-block">
<h4>请求内容</h4>
<pre class="log-json">{{ pretty(detail.requestBody) || '—' }}</pre>
</div>
<div class="log-block">
<h4>响应内容</h4>
<pre class="log-json">{{ pretty(detail.responseBody) || '—' }}</pre>
</div>
<div v-if="detail.errorMsg" class="log-block">
<h4>错误信息</h4>
<pre class="log-json">{{ detail.errorMsg }}</pre>
</div>
</template>
</div>
</el-dialog>
</template>
+58 -7
View File
@@ -1,17 +1,68 @@
<script setup>
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listConfigsApi, updateConfigsApi } from '@/api/admin/config'
import { useRbac } from '@/stores/rbac'
const { can } = useRbac()
const loading = ref(false)
const saving = ref(false)
const workspaceName = ref('')
const defaultStatus = ref('draft')
const statusOptions = [
{ code: 'draft', text: '草稿' },
{ code: 'reviewing', text: '待审核' }
]
async function load() {
loading.value = true
try {
const res = await listConfigsApi()
const map = Object.fromEntries((res.data || []).map((item) => [item.key, item.value]))
workspaceName.value = map['workspace.name'] || ''
defaultStatus.value = map['content.default-status'] || 'draft'
} finally {
loading.value = false
}
}
async function save() {
saving.value = true
try {
await updateConfigsApi({
'workspace.name': workspaceName.value,
'content.default-status': defaultStatus.value
})
ElMessage.success('设置已保存')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<div class="page-heading"
><div><div class="eyebrow">系统管理</div><h1>系统设置</h1><p>工作空间基础设置。</p></div></div
>
<section class="panel settings-panel"
<section v-loading="loading" class="panel settings-panel"
><h2>工作空间</h2
><el-form label-position="top" class="settings-form"
><el-form-item label="空间名称"><el-input model-value="ReelShort Studio" /></el-form-item
><el-form-item label="空间名称"
><el-input v-model="workspaceName" placeholder="请输入工作空间名称" /></el-form-item
><el-form-item label="内容默认状态"
><el-select model-value="草稿"
><el-option label="草稿" value="草稿" /><el-option
label="待审核"
value="待审核" /></el-select></el-form-item
><el-button type="primary" disabled>保存设置</el-button></el-form
><el-select v-model="defaultStatus"
><el-option
v-for="item in statusOptions"
:key="item.code"
:label="item.text"
:value="item.code" /></el-select></el-form-item
><el-button type="primary" :loading="saving" :disabled="!can('system:role')" @click="save"
>保存设置</el-button
></el-form
></section
>
</template>