迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
List as ListIcon,
|
||||
Key as KeyIcon,
|
||||
SwitchButton as SwitchIcon,
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('adminToken')
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 登录页独立全屏,不套后台布局 -->
|
||||
<router-view v-if="route.path === '/login'" />
|
||||
|
||||
<el-container v-else class="app-layout">
|
||||
<el-aside width="200px" class="app-aside">
|
||||
<div class="app-logo">视野管理端</div>
|
||||
<el-menu :default-active="route.path" router>
|
||||
<el-menu-item index="/orders">
|
||||
<el-icon><ListIcon /></el-icon>
|
||||
<span>订单管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/licenses">
|
||||
<el-icon><KeyIcon /></el-icon>
|
||||
<span>授权管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="app-header">
|
||||
<span class="app-header-title">{{ route.meta.title }}</span>
|
||||
<el-button link :icon="SwitchIcon" class="app-header-logout" @click="logout">退出登录</el-button>
|
||||
</el-header>
|
||||
<el-main class="app-main">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
.app-aside {
|
||||
background: #001529;
|
||||
}
|
||||
.app-logo {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.app-aside :deep(.el-menu) {
|
||||
border-right: none;
|
||||
background: #001529;
|
||||
}
|
||||
.app-aside :deep(.el-menu-item) {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.app-aside :deep(.el-menu-item.is-active),
|
||||
.app-aside :deep(.el-menu-item:hover) {
|
||||
color: #fff;
|
||||
background: #1890ff;
|
||||
}
|
||||
.app-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.app-header-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.app-main {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '../router'
|
||||
|
||||
// 管理端请求封装:统一 /api/v1/admin 前缀;token 由登录页输入存 localStorage,随请求携带
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1/admin',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
request.interceptors.request.use((config) => {
|
||||
// 登录页校验时 localStorage 尚无 token,用请求级 authToken 优先
|
||||
const token = config.authToken || localStorage.getItem('adminToken')
|
||||
if (token) {
|
||||
config.headers['X-Admin-Token'] = token
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
request.interceptors.response.use(
|
||||
(resp) => {
|
||||
const body = resp.data
|
||||
if (body && body.code === 0) {
|
||||
return body.data
|
||||
}
|
||||
const msg = (body && body.message) || '请求失败'
|
||||
ElMessage.error(msg)
|
||||
return Promise.reject(new Error(msg))
|
||||
},
|
||||
(err) => {
|
||||
const body = err.response && err.response.data
|
||||
const code = body && body.code
|
||||
// 登录页校验 token 失败:不弹全局错误、不跳转,由页面自行提示
|
||||
if (code === 61 && !(err.config && err.config.skipAuthError)) {
|
||||
localStorage.removeItem('adminToken')
|
||||
ElMessage.error('登录已失效,请重新输入管理 token')
|
||||
router.push('/login')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
ElMessage.error((body && body.message) || err.message || '网络错误')
|
||||
return Promise.reject(err)
|
||||
},
|
||||
)
|
||||
|
||||
export default request
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(ElementPlus, { locale: zhCn }).use(router).mount('#app')
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '../views/Login.vue'
|
||||
import Orders from '../views/Orders.vue'
|
||||
import Licenses from '../views/Licenses.vue'
|
||||
|
||||
// 生产部署于 /admin/ 前缀(后端静态托管),history 路由由后端 SPA fallback 兜底
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/admin/'),
|
||||
routes: [
|
||||
{ path: '/login', name: 'login', component: Login, meta: { title: '登录' } },
|
||||
{ path: '/', redirect: '/orders' },
|
||||
{ path: '/orders', name: 'orders', component: Orders, meta: { title: '订单管理' } },
|
||||
{ path: '/licenses', name: 'licenses', component: Licenses, meta: { title: '授权管理' } },
|
||||
],
|
||||
})
|
||||
|
||||
// 登录守卫:未输入 token 一律回登录页
|
||||
router.beforeEach((to) => {
|
||||
if (to.path !== '/login' && !localStorage.getItem('adminToken')) {
|
||||
return '/login'
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
document.title = (to.meta.title ? to.meta.title + ' - ' : '') + '视野管理端'
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,17 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
color: #303133;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../api/request'
|
||||
|
||||
const filter = reactive({ phoneNum: '' })
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
|
||||
const grantVisible = ref(false)
|
||||
const grantForm = reactive({ phoneNum: '', planId: 'day' })
|
||||
const granting = ref(false)
|
||||
|
||||
const planOptions = [
|
||||
{ value: 'day', label: '1天' },
|
||||
{ value: 'week', label: '7天' },
|
||||
{ value: 'month', label: '30天' },
|
||||
]
|
||||
|
||||
function load() {
|
||||
loading.value = true
|
||||
request
|
||||
.get('/licenses', {
|
||||
params: {
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
phoneNum: filter.phoneNum || undefined,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function openGrant(row) {
|
||||
grantForm.phoneNum = row.phoneNum
|
||||
grantForm.planId = 'day'
|
||||
grantVisible.value = true
|
||||
}
|
||||
|
||||
function submitGrant() {
|
||||
granting.value = true
|
||||
request
|
||||
.post('/licenses/grant', { phoneNum: grantForm.phoneNum.trim(), planId: grantForm.planId })
|
||||
.then((data) => {
|
||||
ElMessage.success(`授权成功,到期时间 ${data.expiresAt}`)
|
||||
grantVisible.value = false
|
||||
load()
|
||||
})
|
||||
.finally(() => {
|
||||
granting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function revoke(row) {
|
||||
ElMessageBox.confirm(`确认撤销 ${row.phoneNum} 的授权?撤销后该账号额度立即失效,账号保留可继续登录。`, '撤销授权', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认撤销',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
.then(() => request.post('/licenses/revoke', { phoneNum: row.phoneNum }))
|
||||
.then(() => {
|
||||
ElMessage.success('已撤销')
|
||||
load()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-form :model="filter" inline @submit.prevent="search">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="filter.phoneNum" placeholder="模糊匹配" clearable style="width: 220px" @keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="filter.phoneNum = ''; search()">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="phoneNum" label="手机号" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="授权状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.expiresAt" type="success" size="small">已授权</el-tag>
|
||||
<el-tag v-else type="info" size="small">未充值</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expiresAt" label="到期时间" width="180">
|
||||
<template #default="{ row }">{{ row.expiresAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="注册时间" width="180">
|
||||
<template #default="{ row }">{{ row.createdAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="180">
|
||||
<template #default="{ row }">{{ row.updatedAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="openGrant(row)">授权</el-button>
|
||||
<el-button type="danger" link size="small" :disabled="!row.expiresAt" @click="revoke(row)">撤销</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pager"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="grantVisible" title="手动授权" width="420px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="手机号">
|
||||
<el-input :model-value="grantForm.phoneNum" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐">
|
||||
<el-select v-model="grantForm.planId" style="width: 100%">
|
||||
<el-option v-for="p in planOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="grantVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="granting" @click="submitGrant">确认授权</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 14px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Lock as LockIcon } from '@element-plus/icons-vue'
|
||||
import request from '../api/request'
|
||||
|
||||
const router = useRouter()
|
||||
const token = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
// 登录 = 用输入的管理 token 调一个轻量接口校验,有效则存 localStorage 进入主页
|
||||
function login() {
|
||||
if (!token.value.trim()) {
|
||||
errorMsg.value = '请输入管理 token'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
request
|
||||
.get('/orders', { authToken: token.value.trim(), skipAuthError: true })
|
||||
.then(() => {
|
||||
localStorage.setItem('adminToken', token.value.trim())
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/orders')
|
||||
})
|
||||
.catch(() => {
|
||||
errorMsg.value = 'token 无效或服务端未配置(检查 config.yml admin.token)'
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<el-card class="login-card" shadow="always">
|
||||
<div class="login-title">视野管理端</div>
|
||||
<el-input
|
||||
v-model="token"
|
||||
type="password"
|
||||
placeholder="请输入管理 token"
|
||||
show-password
|
||||
size="large"
|
||||
:prefix-icon="LockIcon"
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
<div v-if="errorMsg" class="login-error">{{ errorMsg }}</div>
|
||||
<el-button type="primary" size="large" class="login-btn" :loading="loading" @click="login">
|
||||
登录
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #001529;
|
||||
}
|
||||
.login-card {
|
||||
width: 380px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
.login-error {
|
||||
color: #f56c6c;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import request from '../api/request'
|
||||
|
||||
const filter = reactive({
|
||||
phoneNum: '',
|
||||
status: '',
|
||||
timeRange: [],
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
|
||||
const statusMap = {
|
||||
created: { text: '待支付', type: 'warning' },
|
||||
paid: { text: '已支付', type: 'success' },
|
||||
closed: { text: '已关闭', type: 'info' },
|
||||
}
|
||||
|
||||
const channelMap = {
|
||||
wechat: { text: '微信', type: 'success' },
|
||||
alipay: { text: '支付宝', type: 'primary' },
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
return t || '-'
|
||||
}
|
||||
|
||||
function load() {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
}
|
||||
if (filter.phoneNum) params.phoneNum = filter.phoneNum
|
||||
if (filter.status) params.status = filter.status
|
||||
if (filter.timeRange && filter.timeRange.length === 2) {
|
||||
params.startAt = filter.timeRange[0]
|
||||
params.endAt = filter.timeRange[1]
|
||||
}
|
||||
request
|
||||
.get('/orders', { params })
|
||||
.then((data) => {
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
filter.phoneNum = ''
|
||||
filter.status = ''
|
||||
filter.timeRange = []
|
||||
search()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-form :model="filter" inline @submit.prevent="search">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="filter.phoneNum" placeholder="精确匹配" clearable style="width: 200px" @keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filter.status" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v.text" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="下单时间">
|
||||
<el-date-picker
|
||||
v-model="filter.timeRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 340px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="orderId" label="订单号" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="phoneNum" label="手机号" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="planId" label="套餐" width="90" />
|
||||
<el-table-column label="渠道" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(channelMap[row.channel] || {}).type || 'info'" size="small">{{ (channelMap[row.channel] || {}).text || row.channel }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="100">
|
||||
<template #default="{ row }">¥{{ (row.amountCents / 100).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(statusMap[row.status] || {}).type || 'info'" size="small">{{ (statusMap[row.status] || {}).text || row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.paidAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pager"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pager {
|
||||
margin-top: 14px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user