打包项目
This commit is contained in:
12
frontend-vue/convert.html
Normal file
12
frontend-vue/convert.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>格式转换 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/convert-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
frontend-vue/dedupe.html
Normal file
12
frontend-vue/dedupe.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据去重 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/dedupe-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
frontend-vue/split.html
Normal file
12
frontend-vue/split.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据拆分 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/split-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
7
frontend-vue/src/convert-main.ts
Normal file
7
frontend-vue/src/convert-main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import BrandConvertTab from '@/pages/brand/components/BrandConvertTab.vue'
|
||||||
|
|
||||||
|
createApp(BrandConvertTab).use(ElementPlus).mount('#app')
|
||||||
7
frontend-vue/src/dedupe-main.ts
Normal file
7
frontend-vue/src/dedupe-main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import BrandDedupeTab from '@/pages/brand/components/BrandDedupeTab.vue'
|
||||||
|
|
||||||
|
createApp(BrandDedupeTab).use(ElementPlus).mount('#app')
|
||||||
@@ -1,584 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="admin-page">
|
|
||||||
<div class="admin-nav">
|
|
||||||
<router-link to="/home">← 返回首页</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="page-title">管理后台</h1>
|
|
||||||
|
|
||||||
<el-tabs v-model="activeTab" class="admin-tabs" @tab-change="handleTabChange">
|
|
||||||
<el-tab-pane label="用户管理" name="users">
|
|
||||||
<div class="admin-grid">
|
|
||||||
<el-card class="form-card">
|
|
||||||
<template #header>创建用户</template>
|
|
||||||
|
|
||||||
<p class="card-tip">无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。</p>
|
|
||||||
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input v-model="createForm.username" placeholder="用户名(至少2个字符)" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="密码">
|
|
||||||
<el-input
|
|
||||||
v-model="createForm.password"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="密码(至少6个字符)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showRoleSelect" label="角色">
|
|
||||||
<el-select v-model="createForm.role">
|
|
||||||
<el-option label="普通号" value="normal" />
|
|
||||||
<el-option label="管理员" value="admin" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showCreatedBySelect" label="所属管理员">
|
|
||||||
<el-select v-model="createForm.createdById" placeholder="请选择管理员">
|
|
||||||
<el-option
|
|
||||||
v-for="admin in adminsList"
|
|
||||||
:key="admin.id"
|
|
||||||
:label="admin.username"
|
|
||||||
:value="admin.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="primary" :loading="createLoading" @click="handleCreateUser">创建用户</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="table-card">
|
|
||||||
<template #header>用户列表</template>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-input
|
|
||||||
v-model="searchUsername"
|
|
||||||
placeholder="输入用户名关键字"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="loadUsers(1)"
|
|
||||||
/>
|
|
||||||
<el-select
|
|
||||||
v-if="isSuperAdmin"
|
|
||||||
v-model="filterCreatedBy"
|
|
||||||
placeholder="所属管理员"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="admin in adminsList"
|
|
||||||
:key="admin.id"
|
|
||||||
:label="admin.username"
|
|
||||||
:value="String(admin.id)"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-button type="primary" @click="loadUsers(1)">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="users" border>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
|
||||||
<el-table-column label="角色" width="120">
|
|
||||||
<template #default="{ row }">{{ roleLabel(row.role) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="creator_username" label="所属管理员" min-width="140">
|
|
||||||
<template #default="{ row }">{{ row.creator_username || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="row-actions">
|
|
||||||
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" plain @click="removeUser(row)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
class="pagination"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="userPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="userTotal"
|
|
||||||
@current-change="loadUsers"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="查看生成记录" name="history">
|
|
||||||
<el-card class="table-card">
|
|
||||||
<template #header>筛选条件</template>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-select v-model="filterUserId" placeholder="全部用户" clearable>
|
|
||||||
<el-option
|
|
||||||
v-for="option in userOptions"
|
|
||||||
:key="option.id"
|
|
||||||
:label="`${option.username} (${roleLabel(option.role)})`"
|
|
||||||
:value="String(option.id)"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-date-picker
|
|
||||||
v-model="filterTimeStart"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="开始时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
<el-date-picker
|
|
||||||
v-model="filterTimeEnd"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="结束时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
<el-button type="primary" @click="loadHistory(1)">查询</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="table-card history-card">
|
|
||||||
<template #header>生成记录</template>
|
|
||||||
|
|
||||||
<el-table :data="historyRows" border>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="username" label="用户" min-width="120">
|
|
||||||
<template #default="{ row }">{{ row.username || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="panel_type" label="类型" min-width="140">
|
|
||||||
<template #default="{ row }">{{ row.panel_type || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
|
||||||
<el-table-column label="结果预览" min-width="220">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="getHistoryPreviewUrls(row).length" class="history-preview">
|
|
||||||
<el-image
|
|
||||||
v-for="(url, index) in getHistoryPreviewUrls(row)"
|
|
||||||
:key="`${row.id}-${index}`"
|
|
||||||
:src="url"
|
|
||||||
fit="cover"
|
|
||||||
:preview-src-list="getHistoryPreviewUrls(row)"
|
|
||||||
class="preview-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
class="pagination"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="historyPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="historyTotal"
|
|
||||||
@current-change="loadHistory"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
|
|
||||||
<el-dialog v-model="editVisible" title="编辑用户" width="420px">
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input :model-value="editForm.username" readonly />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新密码(不修改留空)">
|
|
||||||
<el-input
|
|
||||||
v-model="editForm.password"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="留空则不修改密码"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showEditRoleSelect" label="角色">
|
|
||||||
<el-select v-model="editForm.role">
|
|
||||||
<el-option label="普通号" value="normal" />
|
|
||||||
<el-option label="管理员" value="admin" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="editForm.creatorUsername" label="所属管理员">
|
|
||||||
<el-input :model-value="editForm.creatorUsername" readonly />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="editVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="editLoading" @click="saveUser">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import {
|
|
||||||
createAdminUser,
|
|
||||||
deleteAdminUser,
|
|
||||||
getAdminHistory,
|
|
||||||
getAdminUsers,
|
|
||||||
updateAdminUser,
|
|
||||||
type AdminHistoryItem,
|
|
||||||
type AdminUserItem,
|
|
||||||
} from '@/shared/api/admin'
|
|
||||||
|
|
||||||
interface UserOption extends AdminUserItem {}
|
|
||||||
|
|
||||||
const activeTab = ref<'users' | 'history'>('users')
|
|
||||||
const pageSize = 15
|
|
||||||
|
|
||||||
const currentUserRole = ref<'super_admin' | 'admin' | 'normal' | string>('admin')
|
|
||||||
const adminsList = ref<{ id: number; username: string }[]>([])
|
|
||||||
const users = ref<AdminUserItem[]>([])
|
|
||||||
const userTotal = ref(0)
|
|
||||||
const userPage = ref(1)
|
|
||||||
const searchUsername = ref('')
|
|
||||||
const filterCreatedBy = ref('')
|
|
||||||
const createLoading = ref(false)
|
|
||||||
|
|
||||||
const createForm = reactive({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
role: 'normal',
|
|
||||||
createdById: undefined as number | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const userOptions = ref<UserOption[]>([])
|
|
||||||
const filterUserId = ref('')
|
|
||||||
const filterTimeStart = ref('')
|
|
||||||
const filterTimeEnd = ref('')
|
|
||||||
const historyRows = ref<AdminHistoryItem[]>([])
|
|
||||||
const historyTotal = ref(0)
|
|
||||||
const historyPage = ref(1)
|
|
||||||
|
|
||||||
const editVisible = ref(false)
|
|
||||||
const editLoading = ref(false)
|
|
||||||
const editForm = reactive({
|
|
||||||
id: 0,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
role: 'normal',
|
|
||||||
creatorUsername: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const isSuperAdmin = computed(() => currentUserRole.value === 'super_admin')
|
|
||||||
const showRoleSelect = computed(() => isSuperAdmin.value)
|
|
||||||
const showCreatedBySelect = computed(
|
|
||||||
() => isSuperAdmin.value && createForm.role === 'normal' && adminsList.value.length > 0,
|
|
||||||
)
|
|
||||||
const showEditRoleSelect = computed(() => isSuperAdmin.value)
|
|
||||||
|
|
||||||
function roleLabel(role?: string) {
|
|
||||||
if (role === 'super_admin') return '超级管理员'
|
|
||||||
if (role === 'admin') return '管理员'
|
|
||||||
return '普通号'
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUsersQuery(page: number) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
page_size: String(pageSize),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (searchUsername.value.trim()) {
|
|
||||||
params.set('username', searchUsername.value.trim())
|
|
||||||
}
|
|
||||||
if (filterCreatedBy.value) {
|
|
||||||
params.set('created_by_id', filterCreatedBy.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers(page = 1) {
|
|
||||||
userPage.value = page
|
|
||||||
try {
|
|
||||||
const result = await getAdminUsers(buildUsersQuery(page))
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '加载用户失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUserRole.value = result.current_user_role || 'admin'
|
|
||||||
adminsList.value = result.admins || []
|
|
||||||
users.value = result.items || []
|
|
||||||
userTotal.value = result.total || 0
|
|
||||||
|
|
||||||
if (!isSuperAdmin.value) {
|
|
||||||
createForm.role = 'normal'
|
|
||||||
createForm.createdById = undefined
|
|
||||||
filterCreatedBy.value = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUserOptions() {
|
|
||||||
try {
|
|
||||||
const result = await getAdminUsers('page=1&page_size=999')
|
|
||||||
if (result.success) {
|
|
||||||
userOptions.value = result.items || []
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
userOptions.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHistoryQuery(page: number) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
page_size: String(pageSize),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (filterUserId.value) {
|
|
||||||
params.set('user_id', filterUserId.value)
|
|
||||||
}
|
|
||||||
if (filterTimeStart.value) {
|
|
||||||
params.set('time_start', filterTimeStart.value)
|
|
||||||
}
|
|
||||||
if (filterTimeEnd.value) {
|
|
||||||
params.set('time_end', filterTimeEnd.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadHistory(page = 1) {
|
|
||||||
historyPage.value = page
|
|
||||||
try {
|
|
||||||
const result = await getAdminHistory(buildHistoryQuery(page))
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '加载记录失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyRows.value = result.items || []
|
|
||||||
historyTotal.value = result.total || 0
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreateUser() {
|
|
||||||
const username = createForm.username.trim()
|
|
||||||
if (username.length < 2) {
|
|
||||||
ElMessage.warning('用户名至少2个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createForm.password.length < 6) {
|
|
||||||
ElMessage.warning('密码至少6个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCreatedBySelect.value && !createForm.createdById) {
|
|
||||||
ElMessage.warning('请选择所属管理员')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createLoading.value = true
|
|
||||||
try {
|
|
||||||
const payload: {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: string
|
|
||||||
created_by_id?: number
|
|
||||||
} = {
|
|
||||||
username,
|
|
||||||
password: createForm.password,
|
|
||||||
role: isSuperAdmin.value ? createForm.role : 'normal',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCreatedBySelect.value && createForm.createdById) {
|
|
||||||
payload.created_by_id = createForm.createdById
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createAdminUser(payload)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '创建失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success(result.msg || '创建成功')
|
|
||||||
createForm.username = ''
|
|
||||||
createForm.password = ''
|
|
||||||
createForm.role = 'normal'
|
|
||||||
createForm.createdById = undefined
|
|
||||||
await loadUsers(1)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
} finally {
|
|
||||||
createLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDialog(user: AdminUserItem) {
|
|
||||||
editForm.id = user.id
|
|
||||||
editForm.username = user.username || ''
|
|
||||||
editForm.password = ''
|
|
||||||
editForm.role = user.role === 'admin' ? 'admin' : 'normal'
|
|
||||||
editForm.creatorUsername = user.creator_username || ''
|
|
||||||
editVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUser() {
|
|
||||||
editLoading.value = true
|
|
||||||
try {
|
|
||||||
const payload: { password?: string; role?: string } = {}
|
|
||||||
if (editForm.password) {
|
|
||||||
if (editForm.password.length < 6) {
|
|
||||||
ElMessage.warning('密码至少6个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload.password = editForm.password
|
|
||||||
}
|
|
||||||
if (showEditRoleSelect.value) {
|
|
||||||
payload.role = editForm.role
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateAdminUser(editForm.id, payload)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '保存失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editVisible.value = false
|
|
||||||
ElMessage.success(result.msg || '保存成功')
|
|
||||||
await loadUsers(userPage.value)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
} finally {
|
|
||||||
editLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeUser(user: AdminUserItem) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除用户 "${user.username || ''}" 吗?`, '删除确认', {
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await deleteAdminUser(user.id)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '删除失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
await loadUsers(userPage.value)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHistoryPreviewUrls(row: AdminHistoryItem) {
|
|
||||||
const urls = row.long_image_url ? [row.long_image_url] : []
|
|
||||||
return urls.concat(row.result_urls || []).slice(0, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabChange(name: string | number) {
|
|
||||||
if (name === 'history') {
|
|
||||||
loadHistory(1).catch(() => undefined)
|
|
||||||
loadUserOptions().catch(() => undefined)
|
|
||||||
} else {
|
|
||||||
loadUsers(1).catch(() => undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadUsers(1)
|
|
||||||
await loadUserOptions()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.admin-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 24px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-tabs {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 360px 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-card,
|
|
||||||
.table-card {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-tip {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar > * {
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 16px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-preview {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.admin-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,166 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="home-page">
|
|
||||||
<header class="home-header">
|
|
||||||
<div class="brand">数富AI</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<span>{{ username }}</span>
|
|
||||||
<el-popover placement="bottom-end" :width="320" trigger="click">
|
|
||||||
<template #reference>
|
|
||||||
<el-button circle>↻</el-button>
|
|
||||||
</template>
|
|
||||||
<div class="update-panel">
|
|
||||||
<div class="update-title">软件更新</div>
|
|
||||||
<div class="update-version">当前版本: v{{ version }}</div>
|
|
||||||
<el-button type="primary" text :loading="loading" @click="check">检测更新</el-button>
|
|
||||||
<div class="update-hint">{{ hint }}</div>
|
|
||||||
<el-button v-if="downloadVisible" type="success" :loading="downloadLoading" @click="handleUpdate">
|
|
||||||
立即更新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<a href="/login" @click.prevent="handleLogout">退出</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="home-main">
|
|
||||||
<div class="entrances">
|
|
||||||
<router-link v-if="isVisible('brand')" class="entrance-card" to="/brand">亚马逊</router-link>
|
|
||||||
<button v-if="isVisible('wb')" class="entrance-card is-disabled" type="button" @click="showComingSoon">
|
|
||||||
wildberries
|
|
||||||
</button>
|
|
||||||
<router-link v-if="isVisible('image')" class="entrance-card" to="/image">图片</router-link>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { logout } from '@/shared/api/auth'
|
|
||||||
import { useUpdateCheck } from '@/shared/composables/useUpdateCheck'
|
|
||||||
import { getBootstrapState } from '@/shared/utils/bootstrap'
|
|
||||||
import { getUserColumnPermissions } from '@/shared/api/admin'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const bootstrap = getBootstrapState()
|
|
||||||
const username = computed(() => bootstrap.username || '用户')
|
|
||||||
const userId = computed(() => bootstrap.userId || '')
|
|
||||||
const visibleKeys = ref<string[]>(['brand', 'wb', 'image'])
|
|
||||||
const { loading, version, hint, downloadVisible, downloadLoading, check, runUpdate } = useUpdateCheck()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!userId.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getUserColumnPermissions(userId.value)
|
|
||||||
if (result.success && Array.isArray(result.items)) {
|
|
||||||
visibleKeys.value = result.items
|
|
||||||
.map((item) => (item.column_key || '').toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// keep default entrances if permissions cannot be loaded
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isVisible = (key: string) => visibleKeys.value.includes(key)
|
|
||||||
|
|
||||||
const showComingSoon = () => {
|
|
||||||
ElMessage.info('暂未开通,敬请期待')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await logout()
|
|
||||||
localStorage.removeItem('maixiang_api_key')
|
|
||||||
window.__BOOTSTRAP__ = {}
|
|
||||||
await router.replace('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
|
||||||
const confirmed = window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')
|
|
||||||
if (!confirmed) return
|
|
||||||
await runUpdate()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.home-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background:
|
|
||||||
linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)),
|
|
||||||
url('/static/bg.jpg') center/cover no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-main {
|
|
||||||
min-height: calc(100vh - 72px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrances {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrance-card {
|
|
||||||
width: 180px;
|
|
||||||
height: 110px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-disabled {
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-version,
|
|
||||||
.update-hint {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="dark">
|
|
||||||
<div class="image-page">
|
|
||||||
<header class="top-bar">
|
|
||||||
<div class="logo-area">
|
|
||||||
<span class="app-name">数富AI</span>
|
|
||||||
<router-link to="/home" class="btn-home">返回首页</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-tabs">
|
|
||||||
<div class="nav-tab-group">
|
|
||||||
<button type="button" class="nav-tab active">图片</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="page-body"></main>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
document.title = '图片 - 数富AI'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.image-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: rgba(13, 13, 13, 1);
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", serif;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar {
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-bottom: 1px solid #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-name {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-home {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-home:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(77, 72, 72, 0.99);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab {
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #3498db;
|
|
||||||
background: rgba(52, 152, 219, 0.2);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-body {
|
|
||||||
height: calc(100vh - 56px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="login-page">
|
|
||||||
<div class="login-header">数富AI</div>
|
|
||||||
<el-card class="login-card" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="login-title">登录</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" class="login-alert" />
|
|
||||||
|
|
||||||
<el-form label-width="72px" @submit.prevent="handleSubmit">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名" autofocus />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="密码">
|
|
||||||
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码"
|
|
||||||
@keydown.enter="handleSubmit" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="primary" :loading="submitting" class="login-button" @click="handleSubmit">
|
|
||||||
登录
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { checkAuth, login } from '@/shared/api/auth'
|
|
||||||
import { getBootstrapState } from '@/shared/utils/bootstrap'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const bootstrap = getBootstrapState()
|
|
||||||
const submitting = ref(false)
|
|
||||||
const errorMessage = ref(bootstrap.error || '')
|
|
||||||
const form = reactive({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const result = await checkAuth()
|
|
||||||
if (!result.logged_in) return
|
|
||||||
|
|
||||||
const target = result.redirect || '/home'
|
|
||||||
if (target.startsWith('/')) {
|
|
||||||
await router.push(target)
|
|
||||||
} else {
|
|
||||||
window.location.href = target
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore auth check failures during bootstrap
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!form.username || !form.password) {
|
|
||||||
errorMessage.value = '请输入用户名和密码'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
errorMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await login(form.username, form.password)
|
|
||||||
if (result.success) {
|
|
||||||
const target = result.redirect || '/home'
|
|
||||||
if (target.startsWith('/')) {
|
|
||||||
await router.push(target)
|
|
||||||
} else {
|
|
||||||
window.location.href = target
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage.value = result.error || '登录失败'
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error instanceof Error ? error.message : '登录失败'
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 16px;
|
|
||||||
background: linear-gradient(180deg, #dce8f1 0%, #eef4f8 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
color: #303133;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-alert {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
redirect: '/home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
component: () => import('@/pages/login/index.vue'),
|
|
||||||
meta: { title: '登录 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home',
|
|
||||||
component: () => import('@/pages/home/index.vue'),
|
|
||||||
meta: { title: '首页 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/brand',
|
|
||||||
component: () => import('@/pages/brand/index.vue'),
|
|
||||||
meta: { title: '亚马逊 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
component: () => import('@/pages/admin/index.vue'),
|
|
||||||
meta: { title: '管理后台 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/image',
|
|
||||||
component: () => import('@/pages/image/index.vue'),
|
|
||||||
meta: { title: '图片 - 数富AI' },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes,
|
|
||||||
})
|
|
||||||
|
|
||||||
router.afterEach((to) => {
|
|
||||||
if (to.meta.title) {
|
|
||||||
document.title = String(to.meta.title)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { requestDeleteJson, requestGetJson, requestPostJson, requestPutJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface AdminUserItem {
|
|
||||||
id: number
|
|
||||||
username?: string
|
|
||||||
role?: 'super_admin' | 'admin' | 'normal' | string
|
|
||||||
creator_username?: string
|
|
||||||
created_at?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminSimpleUser {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminUsersResponse {
|
|
||||||
success: boolean
|
|
||||||
current_user_role?: 'super_admin' | 'admin' | 'normal' | string
|
|
||||||
admins?: AdminSimpleUser[]
|
|
||||||
items?: AdminUserItem[]
|
|
||||||
total?: number
|
|
||||||
page?: number
|
|
||||||
page_size?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminMutationResponse {
|
|
||||||
success: boolean
|
|
||||||
msg?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminHistoryItem {
|
|
||||||
id: number
|
|
||||||
username?: string
|
|
||||||
panel_type?: string
|
|
||||||
created_at?: string
|
|
||||||
long_image_url?: string | null
|
|
||||||
result_urls?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminHistoryResponse {
|
|
||||||
success: boolean
|
|
||||||
items?: AdminHistoryItem[]
|
|
||||||
total?: number
|
|
||||||
page?: number
|
|
||||||
page_size?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminColumnPermissionItem {
|
|
||||||
column_key?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminColumnPermissionResponse {
|
|
||||||
success: boolean
|
|
||||||
items?: AdminColumnPermissionItem[]
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminUsers(query = '') {
|
|
||||||
const suffix = query ? `?${query}` : ''
|
|
||||||
return requestGetJson<AdminUsersResponse>(`/api/admin/users${suffix}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAdminUser(payload: {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: string
|
|
||||||
created_by_id?: number
|
|
||||||
}) {
|
|
||||||
return requestPostJson<AdminMutationResponse>('/api/admin/user', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateAdminUser(
|
|
||||||
userId: number | string,
|
|
||||||
payload: {
|
|
||||||
password?: string
|
|
||||||
role?: string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return requestPutJson<AdminMutationResponse>(`/api/admin/user/${userId}`, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteAdminUser(userId: number | string) {
|
|
||||||
return requestDeleteJson<AdminMutationResponse>(`/api/admin/user/${userId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminHistory(query = '') {
|
|
||||||
const suffix = query ? `?${query}` : ''
|
|
||||||
return requestGetJson<AdminHistoryResponse>(`/api/admin/history${suffix}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserColumnPermissions(userId: string | number) {
|
|
||||||
return requestGetJson<AdminColumnPermissionResponse>(`/api/admin/user/${userId}/column-permissions`)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
success: boolean
|
|
||||||
redirect?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthCheckResponse {
|
|
||||||
logged_in: boolean
|
|
||||||
redirect?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkAuth() {
|
|
||||||
return requestGetJson<AuthCheckResponse>('/api/auth/check')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.set('username', username)
|
|
||||||
formData.set('password', password)
|
|
||||||
|
|
||||||
return requestPostJson<LoginResponse>('/login', formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
try {
|
|
||||||
await requestGetJson('/logout', {
|
|
||||||
validateStatus: () => true,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// logout should still continue on the client even if backend responds abnormally
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface UploadFileVo {
|
|
||||||
fileKey: string
|
|
||||||
originalFilename?: string
|
|
||||||
localPath?: string
|
|
||||||
size?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertTemplateVo {
|
|
||||||
id: number
|
|
||||||
templateCode: string
|
|
||||||
templateName: string
|
|
||||||
outputFilename: string
|
|
||||||
isDefault?: boolean
|
|
||||||
builtIn?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UploadedSourceFileDto {
|
|
||||||
fileKey: string
|
|
||||||
originalFilename?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertResultItemVo {
|
|
||||||
resultId?: number
|
|
||||||
sourceFilename?: string
|
|
||||||
outputFilename?: string
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
downloadUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertRunVo {
|
|
||||||
total: number
|
|
||||||
successCount: number
|
|
||||||
failedCount: number
|
|
||||||
items: ConvertResultItemVo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StandardApiResponse<T> {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConvertTemplates() {
|
|
||||||
return requestGetJson<StandardApiResponse<ConvertTemplateVo[]>>('/api/convert/templates')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uploadTempFile(file: File) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
return requestPostJson<StandardApiResponse<UploadFileVo>, FormData>('/api/files/upload', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runConvert(files: UploadedSourceFileDto[], templateId: number) {
|
|
||||||
return requestPostJson<StandardApiResponse<ConvertRunVo>>('/api/convert/run', {
|
|
||||||
files,
|
|
||||||
templateId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { requestGetJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface DedupeHistoryItem {
|
|
||||||
sourceFilename?: string
|
|
||||||
outputFilename?: string
|
|
||||||
downloadUrl?: string
|
|
||||||
success: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DedupeHistoryVo {
|
|
||||||
items: DedupeHistoryItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StandardApiResponse<T> {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDedupeHistory() {
|
|
||||||
return requestGetJson<StandardApiResponse<DedupeHistoryVo>>('/api/dedupe/history')
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface VersionResponse {
|
|
||||||
version?: string
|
|
||||||
has_update?: boolean
|
|
||||||
latest_version?: string
|
|
||||||
desc?: string
|
|
||||||
file_url?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateStartResponse {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchVersion() {
|
|
||||||
return requestGetJson<VersionResponse>('/api/version')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startUpdate(fileUrl: string) {
|
|
||||||
return requestPostJson<UpdateStartResponse>('/api/update/do', { file_url: fileUrl })
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ export interface SplitExcelPayload {
|
|||||||
split_mode: 'rows_per_file' | 'parts'
|
split_mode: 'rows_per_file' | 'parts'
|
||||||
rows_per_file?: number
|
rows_per_file?: number
|
||||||
parts?: number
|
parts?: number
|
||||||
output_dir: string
|
output_dir?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SplitExcelResultItem {
|
export interface SplitExcelResultItem {
|
||||||
@@ -37,6 +37,14 @@ export interface ExcelInfoResponse {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CleanExcelPayload {
|
||||||
|
paths: string[]
|
||||||
|
selected_columns: string[]
|
||||||
|
keep_integer_ids?: boolean
|
||||||
|
keep_underscore_ids?: boolean
|
||||||
|
output_dir?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CleanExcelResultItem {
|
export interface CleanExcelResultItem {
|
||||||
result_id?: number
|
result_id?: number
|
||||||
source_path: string
|
source_path: string
|
||||||
@@ -47,7 +55,7 @@ export interface CleanExcelResultItem {
|
|||||||
|
|
||||||
export interface ConvertTxtPayload {
|
export interface ConvertTxtPayload {
|
||||||
paths: string[]
|
paths: string[]
|
||||||
output_dir: string
|
output_dir?: string
|
||||||
template_id: string
|
template_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
import { fetchVersion, startUpdate } from '@/shared/api/update'
|
|
||||||
|
|
||||||
export function useUpdateCheck() {
|
|
||||||
const loading = ref(false)
|
|
||||||
const version = ref('1.0.0')
|
|
||||||
const hint = ref('')
|
|
||||||
const downloadVisible = ref(false)
|
|
||||||
const downloadLoading = ref(false)
|
|
||||||
const fileUrl = ref('')
|
|
||||||
|
|
||||||
const check = async () => {
|
|
||||||
loading.value = true
|
|
||||||
hint.value = '正在检测更新...'
|
|
||||||
downloadVisible.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchVersion()
|
|
||||||
version.value = (result.version || version.value).replace(/^v/i, '')
|
|
||||||
|
|
||||||
if (result.has_update && result.latest_version) {
|
|
||||||
hint.value = `发现新版本 v${result.latest_version}${result.desc ? `:${result.desc}` : ''}`
|
|
||||||
fileUrl.value = result.file_url || ''
|
|
||||||
downloadVisible.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hint.value = '已是最新版本'
|
|
||||||
} catch (error) {
|
|
||||||
hint.value = error instanceof Error ? error.message : '检测更新失败'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runUpdate = async () => {
|
|
||||||
if (!fileUrl.value) {
|
|
||||||
hint.value = '暂无下载地址,请关注官方渠道。'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadLoading.value = true
|
|
||||||
hint.value = '正在下载并准备更新,程序将自动退出...'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await startUpdate(fileUrl.value)
|
|
||||||
if (result.success) {
|
|
||||||
hint.value = '更新已启动,程序即将退出...'
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
hint.value = result.error || '更新启动失败'
|
|
||||||
return false
|
|
||||||
} catch (error) {
|
|
||||||
hint.value = error instanceof Error ? error.message : '请求更新失败,请重试'
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
downloadLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
version,
|
|
||||||
hint,
|
|
||||||
downloadVisible,
|
|
||||||
downloadLoading,
|
|
||||||
check,
|
|
||||||
runUpdate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { createApp } from 'vue'
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import '@/styles/main.css'
|
import '@/styles/main.css'
|
||||||
import App from '@/App.vue'
|
import BrandSplitTab from '@/pages/brand/components/BrandSplitTab.vue'
|
||||||
import router from '@/router'
|
|
||||||
|
|
||||||
createApp(App).use(ElementPlus).use(router).mount('#app')
|
createApp(BrandSplitTab).use(ElementPlus).mount('#app')
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
@@ -6,6 +7,7 @@ import Components from 'unplugin-vue-components/vite'
|
|||||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
@@ -47,5 +49,19 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
dedupe: resolve(__dirname, 'dedupe.html'),
|
||||||
|
convert: resolve(__dirname, 'convert.html'),
|
||||||
|
split: resolve(__dirname, 'split.html'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'assets/[name].js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ def brand_page():
|
|||||||
return _render_html('brand.html')
|
return _render_html('brand.html')
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route('/brand-tools')
|
||||||
|
@login_required
|
||||||
|
def brand_tools_page():
|
||||||
|
html_path = os.path.join(STATIC_DIR, 'brand-tools', 'brand-tools.html')
|
||||||
|
if os.path.isfile(html_path):
|
||||||
|
return send_file(html_path)
|
||||||
|
return _render_html('brand_tools.html')
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/static/<path:filename>')
|
@main_bp.route('/static/<path:filename>')
|
||||||
def serve_static(filename):
|
def serve_static(filename):
|
||||||
"""提供 static 目录及子目录下的静态文件访问。"""
|
"""提供 static 目录及子目录下的静态文件访问。"""
|
||||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
14
source_code/web_source/brand_tools.html
Normal file
14
source_code/web_source/brand_tools.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="refresh" content="0; url=/static/brand-tools/brand-tools.html" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>品牌工具 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.location.replace('/static/brand-tools/brand-tools.html')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user