diff --git a/.env.example b/.env.example index c49561a..d754c1e 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/.gitignore b/.gitignore index a547bf3..5cbc626 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ lerna-debug.log* node_modules dist dist-ssr +.env.local +.env.*.local *.local # Editor directories and files diff --git a/src/App.vue b/src/App.vue index a78fb6f..14949a0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,4 +4,4 @@ import { RouterView } from 'vue-router' + \ No newline at end of file diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts index 407f5f1..27da3a5 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -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 { diff --git a/src/main.ts b/src/main.ts index 6db4961..03df983 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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') diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index bc7a28c..9dfb610 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -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([]) const tracksLoading = ref(false) const channels = ref([]) const channelsLoading = ref(false) +const publicSourceChannels = ref([]) +const publicSourceChannelsLoading = ref(false) const applications = ref([]) const selectedApplications = ref([]) 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(() => { { :value="channel.id" /> + + +
@@ -1499,7 +1541,7 @@ onMounted(() => { - + + + + @@ -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; } diff --git a/vite.config.ts b/vite.config.ts index 90eb773..9c724fa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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, + }, }, }, - }, -})) + } +})