Merge branch 'master' of ssh://47.101.48.251/data/git/xxfcyds2026/xxfcyds2026-frontend

master
lion 3 months ago
commit e216a68250

@ -1,4 +1,10 @@
# 复制为 .env 或 .env.local 后生效(须以 VITE_ 开头才会注入构建)
# 复制为 .env.local 后生效;.env.local 已加入 .gitignore,适合放本机个性化配置。
# 注意:只有 VITE_ 开头的变量会注入浏览器;FRONTEND_BUILD_OUT_DIR 只供 vite.config.ts 读取。
#
# 管理端生产构建输出目录;可写相对 frontend/ 的路径或绝对路径。
# 不设置时默认输出到 frontend/dist/admin,避免误写到某个固定 backend 仓库。
# 例:FRONTEND_BUILD_OUT_DIR=../backend/public/admin
# FRONTEND_BUILD_OUT_DIR=
#
# 留空则开发时走当前站点同源 + Vite 代理 /api(推荐)
# VITE_API_BASE=

2
.gitignore vendored

@ -10,6 +10,8 @@ lerna-debug.log*
node_modules
dist
dist-ssr
.env.local
.env.*.local
*.local
# Editor directories and files

@ -4,4 +4,4 @@ import { RouterView } from 'vue-router'
<template>
<RouterView />
</template>
</template>

@ -102,6 +102,7 @@ export interface AdminApplicationListParams {
track?: string
keyword?: string
signup_channel_id?: number
public_source_channel_id?: number
}
export type AdminApplicationExportMode = 'xlsx' | 'selected' | 'all'
@ -140,6 +141,9 @@ export interface AdminApplicationRow {
submitted_review_count: number
team_sum: number | null
team_avg: number | null
public_source_channel: { id: number; source_code: string; source_name: string } | null
public_source_code: string | null
public_source_attributed_at: string | null
}
export interface AdminApplicationFileRow {

@ -18,4 +18,6 @@ router.afterEach(() => {
void capturePublicSourceFromLocation()
})
document.documentElement.dataset.adminBuildMarker = '2026-06-19-frontend-rebuild'
createApp(App).use(pinia).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')

@ -20,6 +20,7 @@ import {
listSignupChannels,
updateSignupChannel,
} from '../../../api/admin/signupChannels'
import { listPublicSourceChannels } from '../../../api/admin/publicSourceChannels'
import {
downloadAdminApplicationFile,
exportAdminApplications,
@ -37,6 +38,7 @@ import type {
CompetitionTrackPayload,
CompetitionTrackRow,
FormSchemaRow,
PublicSourceChannelRow,
SignupChannelPayload,
SignupChannelRow,
} from '../../../api/admin/types'
@ -99,6 +101,8 @@ const tracks = ref<CompetitionTrackRow[]>([])
const tracksLoading = ref(false)
const channels = ref<SignupChannelRow[]>([])
const channelsLoading = ref(false)
const publicSourceChannels = ref<PublicSourceChannelRow[]>([])
const publicSourceChannelsLoading = ref(false)
const applications = ref<AdminApplicationRow[]>([])
const selectedApplications = ref<AdminApplicationRow[]>([])
const applicationsLoading = ref(false)
@ -117,6 +121,7 @@ const applicationFilters = ref({
status: 'submitted',
track: '',
signup_channel_id: undefined as number | undefined,
public_source_channel_id: undefined as number | undefined,
})
const trackDialogVisible = ref(false)
@ -289,6 +294,7 @@ watch(tabKey, (t) => {
if (t === 'channels') void refreshChannels()
if (t === 'applications') {
void refreshChannels()
void refreshPublicSourceChannels()
void refreshApplications()
}
if (t === 'signupForm') void refreshSignupSchemas()
@ -637,6 +643,18 @@ async function refreshChannels() {
}
}
async function refreshPublicSourceChannels() {
publicSourceChannelsLoading.value = true
try {
publicSourceChannels.value = await listPublicSourceChannels()
} catch (e) {
publicSourceChannels.value = []
ElMessage.error(e instanceof Error ? e.message : '加载用户渠道列表失败')
} finally {
publicSourceChannelsLoading.value = false
}
}
function applicationStatusLabel(status: string): string {
if (status === 'submitted') return '已提交'
if (status === 'draft') return '草稿'
@ -714,6 +732,7 @@ async function refreshApplications(page = applicationPager.value.page) {
status: applicationFilters.value.status || undefined,
track: applicationFilters.value.track || undefined,
signup_channel_id: applicationFilters.value.signup_channel_id || undefined,
public_source_channel_id: applicationFilters.value.public_source_channel_id || undefined,
})
applications.value = res.data
applicationPager.value = {
@ -730,7 +749,13 @@ async function refreshApplications(page = applicationPager.value.page) {
}
function resetApplicationFilters() {
applicationFilters.value = { keyword: '', status: 'submitted', track: '', signup_channel_id: undefined }
applicationFilters.value = {
keyword: '',
status: 'submitted',
track: '',
signup_channel_id: undefined,
public_source_channel_id: undefined,
}
void refreshApplications(1)
}
@ -752,6 +777,7 @@ async function exportApplications(mode: AdminApplicationExportMode) {
status: applicationFilters.value.status || undefined,
track: applicationFilters.value.track || undefined,
signup_channel_id: applicationFilters.value.signup_channel_id || undefined,
public_source_channel_id: applicationFilters.value.public_source_channel_id || undefined,
application_ids:
mode === 'selected' ? selectedApplications.value.map((row) => row.id) : undefined,
},
@ -1430,7 +1456,7 @@ onMounted(() => {
</el-select>
<el-select
v-model="applicationFilters.signup_channel_id"
placeholder="渠道"
placeholder="报名渠道"
clearable
filterable
teleported
@ -1443,6 +1469,22 @@ onMounted(() => {
:value="channel.id"
/>
</el-select>
<el-select
v-model="applicationFilters.public_source_channel_id"
placeholder="用户渠道"
clearable
filterable
teleported
:loading="publicSourceChannelsLoading"
class="application-filter-select application-filter-channel"
>
<el-option
v-for="channel in publicSourceChannels"
:key="channel.id"
:label="`${channel.source_name}(${channel.source_code})`"
:value="channel.id"
/>
</el-select>
</div>
<div class="application-actions">
<el-dropdown trigger="click" @command="exportApplications">
@ -1499,7 +1541,7 @@ onMounted(() => {
</template>
</el-table-column>
<el-table-column prop="track_title" label="赛道" min-width="130" show-overflow-tooltip />
<el-table-column label="渠道" min-width="150" show-overflow-tooltip>
<el-table-column label="报名渠道" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="cell-muted">
{{
@ -1516,6 +1558,17 @@ onMounted(() => {
</template>
</el-table-column>
<el-table-column prop="contact_mobile" label="手机号" width="128" />
<el-table-column label="用户渠道" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="cell-muted">
{{
row.public_source_channel
? `${row.public_source_channel.source_name}(${row.public_source_channel.source_code})`
: row.public_source_code || '—'
}}
</span>
</template>
</el-table-column>
<el-table-column label="附件" width="76" align="right">
<template #default="{ row }">{{ row.files_count }}</template>
</el-table-column>
@ -2204,7 +2257,7 @@ onMounted(() => {
.application-filters {
display: grid;
grid-template-columns: minmax(220px, 1fr) 130px 130px minmax(180px, 220px);
grid-template-columns: minmax(220px, 1fr) repeat(4, minmax(130px, 180px));
gap: 8px;
align-items: center;
}

@ -2,13 +2,23 @@
import history from 'connect-history-api-fallback'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import type { Plugin } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const laravelPublicAdmin = path.resolve(__dirname, '../xxfcyds2026-backend/public/admin')
function resolveBuildOutDir(rawPath: string | undefined): string {
const configured = rawPath?.trim()
if (!configured) {
return path.resolve(__dirname, 'dist/admin')
}
return path.isAbsolute(configured)
? configured
: path.resolve(__dirname, configured)
}
function spaHistoryFallback(): Plugin {
const middleware = history({ index: '/index.html' })
@ -28,42 +38,47 @@ function spaHistoryFallback(): Plugin {
}
// https://vite.dev/config/
export default defineConfig(({ mode }) => ({
base: mode === 'production' ? '/admin/' : '/',
build: {
outDir: laravelPublicAdmin,
emptyOutDir: true,
},
plugins: [
vue(),
spaHistoryFallback(),
/** 勿在 dev 对 /frontend/prototype 挂 sirv:会与 import prototype.css 冲突(返回 text/css 导致动态 import .vue 失败) */
viteStaticCopy({
targets: [
{
src: 'frontend/prototype/**/*',
dest: 'frontend/prototype',
rename: { stripBase: 2 },
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, '')
const adminOutDir = resolveBuildOutDir(env.FRONTEND_BUILD_OUT_DIR)
return {
base: mode === 'production' ? '/admin/' : '/',
build: {
outDir: adminOutDir,
emptyOutDir: true,
},
plugins: [
vue(),
spaHistoryFallback(),
/** 勿在 dev 对 /frontend/prototype 挂 sirv:会与 import prototype.css 冲突(返回 text/css 导致动态 import .vue 失败) */
viteStaticCopy({
targets: [
{
src: 'frontend/prototype/**/*',
dest: 'frontend/prototype',
rename: { stripBase: 2 },
},
],
}),
],
server: {
open: '/',
proxy: {
'/api': {
// cxxfds-service;与本机 php artisan serve 端口一致(8000 常被其它 Laravel 占用时改用 8001)
target: 'http://127.0.0.1:8002',
changeOrigin: true,
},
],
}),
],
server: {
open: '/',
proxy: {
'/api': {
// cxxfds-service;与本机 php artisan serve 端口一致(8000 常被其它 Laravel 占用时改用 8001)
target: 'http://127.0.0.1:8002',
changeOrigin: true,
},
},
},
preview: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8002',
changeOrigin: true,
preview: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8002',
changeOrigin: true,
},
},
},
},
}))
}
})

Loading…
Cancel
Save