迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)

This commit is contained in:
2026-08-24 12:35:24 +08:00
parent d056f01965
commit 961523d94c
218 changed files with 13391 additions and 3232 deletions
+163
View File
@@ -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>
+86
View File
@@ -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>
+139
View File
@@ -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>