diff --git a/call-client/.env.development b/call-client/.env.development new file mode 100644 index 0000000..8001efc --- /dev/null +++ b/call-client/.env.development @@ -0,0 +1,3 @@ +# 开发模式:Vite 将 /api 代理到此处(见 electron.vite.config.ts) +# 修改后需重启 npm run dev +VITE_DEV_PROXY_TARGET=http://192.168.1.10:8845 diff --git a/call-client/.prettierrc b/call-client/.prettierrc new file mode 100644 index 0000000..8b27645 --- /dev/null +++ b/call-client/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/call-client/electron.vite.config.ts b/call-client/electron.vite.config.ts index ee16d95..0d2109a 100644 --- a/call-client/electron.vite.config.ts +++ b/call-client/electron.vite.config.ts @@ -1,16 +1,34 @@ import vue from '@vitejs/plugin-vue' -import { defineConfig } from 'electron-vite' +import { defineConfig, loadEnv } from 'electron-vite' import { resolve } from 'path' -export default defineConfig({ - main: {}, - preload: {}, - renderer: { - resolve: { - alias: { - '@renderer': resolve('src/renderer/src'), +/** + * 开发时页面在 Vite(如 localhost:5173),若 axios 直连 localhost:8845 会触发浏览器 CORS。 + * 将 /api 代理到真实后端,配合 service.ts 在 DEV 下使用同源路径 /api/queue/caller。 + * 联调其它地址时在项目根 .env.development 设置:VITE_DEV_PROXY_TARGET=http://host:8845 + */ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const devProxyTarget = env.VITE_DEV_PROXY_TARGET || 'http://127.0.0.1:8845' + + return { + main: {}, + preload: {}, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer/src'), + }, + }, + plugins: [vue()], + server: { + proxy: { + '/api': { + target: devProxyTarget, + changeOrigin: true, + }, + }, }, }, - plugins: [vue()], - }, + } }) diff --git a/call-client/src/main/index.ts b/call-client/src/main/index.ts index 6295e97..1de6260 100644 --- a/call-client/src/main/index.ts +++ b/call-client/src/main/index.ts @@ -80,7 +80,7 @@ app.whenReady().then(() => { }) // TicketList -> Main.vue 触发:用于“在票号列表窗口点呼叫/评价后,把主界面对应按钮逻辑执行起来” - ipcMain.on('ticket:main-action', (_event, action: unknown) => { + ipcMain.on('ticket:main-action', (_event, action: unknown, payload: unknown) => { if (!mainWindow) return if (action !== 'call' && action !== 'evaluate') return @@ -88,7 +88,7 @@ app.whenReady().then(() => { try { mainWindow.show() mainWindow.focus() - mainWindow.webContents.send('main:ticket-action', action) + mainWindow.webContents.send('main:ticket-action', action, payload) } catch { // ignore } diff --git a/call-client/src/preload/index.ts b/call-client/src/preload/index.ts index a6ba0e4..17949ff 100644 --- a/call-client/src/preload/index.ts +++ b/call-client/src/preload/index.ts @@ -63,26 +63,37 @@ contextBridge.exposeInMainWorld('nativeDialog', { ipcRenderer.invoke('dialog:confirm', options), }) +type MainTicketAction = 'call' | 'evaluate' +type MainTicketActionPayload = { + ticketUid?: number + tktNum?: string +} + // TicketList -> Main.vue 触发(call/evaluate) contextBridge.exposeInMainWorld('ticketToMain', { - triggerCall: () => ipcRenderer.send('ticket:main-action', 'call'), - triggerEvaluate: () => ipcRenderer.send('ticket:main-action', 'evaluate'), + triggerCall: (payload?: MainTicketActionPayload) => ipcRenderer.send('ticket:main-action', 'call', payload), + triggerEvaluate: (payload?: MainTicketActionPayload) => + ipcRenderer.send('ticket:main-action', 'evaluate', payload), }) let currentMainTicketListener: - | ((event: Electron.IpcRendererEvent, action: 'call' | 'evaluate') => void) + | (( + event: Electron.IpcRendererEvent, + action: MainTicketAction, + payload?: MainTicketActionPayload, + ) => void) | null = null contextBridge.exposeInMainWorld('mainTicketEvents', { - onAction: (callback: (action: 'call' | 'evaluate') => void) => { + onAction: (callback: (action: MainTicketAction, payload?: MainTicketActionPayload) => void) => { if (currentMainTicketListener) { ipcRenderer.removeListener('main:ticket-action', currentMainTicketListener) } - currentMainTicketListener = (event, action) => { + currentMainTicketListener = (event, action, payload) => { void event if (action !== 'call' && action !== 'evaluate') return - callback(action) + callback(action, payload) } ipcRenderer.on('main:ticket-action', currentMainTicketListener) diff --git a/call-client/src/renderer/index.html b/call-client/src/renderer/index.html index 63984d2..11712dd 100644 --- a/call-client/src/renderer/index.html +++ b/call-client/src/renderer/index.html @@ -4,9 +4,10 @@ Electron + diff --git a/call-client/src/renderer/src/api/index.ts b/call-client/src/renderer/src/api/index.ts index f6fc47b..d842302 100644 --- a/call-client/src/renderer/src/api/index.ts +++ b/call-client/src/renderer/src/api/index.ts @@ -1,4 +1,10 @@ import type { CallRequest, CallResponse, PauseRequest, ReCallRequest } from '@renderer/types/action' +import type { + IsRankData, + IsRankRequest, + QueueCountData, + QueueCountRequest, +} from '@renderer/types/rank' import type { TicketPoolRequest, TicketPoolResponse } from '@renderer/types/ticket' import type { UserRequest, UserResponse } from '@renderer/types/user' import type { WindowResponse } from '@renderer/types/window' @@ -17,6 +23,7 @@ export const api = { action: { call: (data: CallRequest) => http.post('/call-terminal/call', data), + init: (data: CallRequest) => http.post('/call-terminal/init', data), recall: (data: ReCallRequest) => http.post('/call-terminal/recall', data), abandon: (data: ReCallRequest) => http.post('/call-terminal/abandon', data), pause: (data: PauseRequest) => http.post('/call-terminal/pause', data), @@ -26,5 +33,12 @@ export const api = { evaluate: (data: ReCallRequest) => http.post('/call-terminal/evaluate', data), pool: (params: TicketPoolRequest) => http.get('/call-terminal/pool', params), + + // evaluating 状态下轮询:判断当前票是否已完成评价/可进入 idle + isRank: (params: IsRankRequest) => http.get('/call-terminal/is-rank', params), + + // idle 状态下轮询:查询当前窗口等候人数 + getQueueCount: (params: QueueCountRequest) => + http.get('/call-terminal/queue-count', params), }, } diff --git a/call-client/src/renderer/src/env.d.ts b/call-client/src/renderer/src/env.d.ts index 72a7349..9f661bf 100644 --- a/call-client/src/renderer/src/env.d.ts +++ b/call-client/src/renderer/src/env.d.ts @@ -55,12 +55,14 @@ interface INativeDialog { } interface ITicketToMain { - triggerCall: () => void - triggerEvaluate: () => void + triggerCall: (payload?: { ticketUid?: number; tktNum?: string }) => void + triggerEvaluate: (payload?: { ticketUid?: number; tktNum?: string }) => void } interface IMainTicketEvents { - onAction: (callback: (action: 'call' | 'evaluate') => void) => void + onAction: ( + callback: (action: 'call' | 'evaluate', payload?: { ticketUid?: number; tktNum?: string }) => void, + ) => void } declare global { diff --git a/call-client/src/renderer/src/types/action.ts b/call-client/src/renderer/src/types/action.ts index fb6f013..fca00f1 100644 --- a/call-client/src/renderer/src/types/action.ts +++ b/call-client/src/renderer/src/types/action.ts @@ -10,6 +10,7 @@ export interface ActionButton { export interface CallRequest { windowUid: number empUid: number + ticketUid?: number | null } export interface CallResponse { diff --git a/call-client/src/renderer/src/types/rank.ts b/call-client/src/renderer/src/types/rank.ts new file mode 100644 index 0000000..3140abf --- /dev/null +++ b/call-client/src/renderer/src/types/rank.ts @@ -0,0 +1,48 @@ +export interface IsRankData { + /** + * 后端返回:当前票是否已评价(hasRank === true 时切换为 idle) + */ + hasRank: boolean + + /** + * 后端回传的票据 UID(可选) + */ + ticketUid?: number + + /** + * 兼容旧字段名(若后端历史上使用过) + */ + isEvaluated?: boolean +} + +export interface IsRankRequest { + /** + * 当前正在办理的 ticketUid(由 Main.vue 的 callingTkt 传入) + */ + ticketUid: number +} + +export interface QueueCountData { + /** + * 当前窗口等候人数 + */ + queueCount: number + + /** + * 后端可能返回其它字段(例如 windowUid),前端不强依赖 + */ + windowUid?: number + + /** + * 兼容旧字段名(如果后端历史上用过 count) + */ + count?: number +} + +export interface QueueCountRequest { + /** + * 当前窗口 UID(由 Login.vue 缓存的 windowUid 传入) + */ + windowUid: number +} + diff --git a/call-client/src/renderer/src/utils/service.ts b/call-client/src/renderer/src/utils/service.ts index b100ca7..376babf 100644 --- a/call-client/src/renderer/src/utils/service.ts +++ b/call-client/src/renderer/src/utils/service.ts @@ -36,6 +36,11 @@ const instance = axios.create({ }) export function applyServerIpToHttp(serverIp: string): void { + // DEV:走当前页面源下的 /api/...,由 Vite 代理到后端,浏览器不跨域,不依赖后端 CORS + if (import.meta.env.DEV) { + instance.defaults.baseURL = API_QUEUE_CALLER_PATH + return + } const url = buildBaseUrlFromServerIp(serverIp) instance.defaults.baseURL = url } diff --git a/call-client/src/renderer/src/views/Login.vue b/call-client/src/renderer/src/views/Login.vue index 7d403b7..efd063d 100644 --- a/call-client/src/renderer/src/views/Login.vue +++ b/call-client/src/renderer/src/views/Login.vue @@ -93,13 +93,25 @@ const handleLogin = async (): Promise => { value: win.windowUid.toString(), } }) - if ( - cachedWinKey.value && - options.value.some(item => item.value === cachedWinKey.value) - ) { + if (cachedWinKey.value && options.value.some(item => item.value === cachedWinKey.value)) { selectedWin.value = cachedWinKey.value } + const initWindowUid = + selectedWin.value.trim() !== '' + ? parseInt(selectedWin.value) + : Number(sessionState.winUid ?? 0) + const initRes = await api.action.init({ + empUid: Number(sessionState.empUid ?? -1), + windowUid: Number.isFinite(initWindowUid) ? initWindowUid : 0, + }) + const initSuccess = + ((initRes as { data?: { success?: boolean } })?.data?.success ?? + (initRes as { success?: boolean })?.success) === true + if (!initSuccess) { + await window.appLogger.log('warn', '初始化接口调用完成,但返回 success=false') + } + // 登录成功,通知主进程登录成功,打开主窗口 isLoginSuccessed.value = true isLoading.value = false @@ -351,7 +363,7 @@ onUnmounted(() => { flex-direction: column; align-items: center; // justify-content: center; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #004d99 0%, #003b7a 100%); overflow: hidden; position: relative; padding: 0px; @@ -484,14 +496,14 @@ onUnmounted(() => { border: 1px solid #dcdfe6; &:hover { - color: #667eea; + color: #004d99; } } &.is-active { .el-radio-button__inner { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-color: #667eea; + background: linear-gradient(135deg, #004d99 0%, #003b7a 100%); + border-color: #004d99; box-shadow: none; } } @@ -517,11 +529,11 @@ onUnmounted(() => { transition: all 0.3s ease; &:hover { - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); + box-shadow: 0 4px 12px rgba(0, 77, 153, 0.15); } &.is-focus { - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25); + box-shadow: 0 4px 12px rgba(0, 77, 153, 0.25); } } } @@ -534,14 +546,14 @@ onUnmounted(() => { font-size: 16px; font-weight: 500; border-radius: 8px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #004d99 0%, #003b7a 100%); border: none; transition: all 0.3s ease; margin-top: 10px; &:hover:not(.is-disabled) { transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); + box-shadow: 0 8px 20px rgba(0, 77, 153, 0.3); } &:active:not(.is-disabled) { @@ -569,10 +581,6 @@ onUnmounted(() => { // 响应式设计 @media (max-width: 480px) { - .login-container { - // padding: 15px; - } - .form-wrapper { padding: 25px 20px !important; } diff --git a/call-client/src/renderer/src/views/Main.vue b/call-client/src/renderer/src/views/Main.vue index e6fd459..81589ea 100644 --- a/call-client/src/renderer/src/views/Main.vue +++ b/call-client/src/renderer/src/views/Main.vue @@ -1,6 +1,7 @@ diff --git a/call-client/src/renderer/src/views/TicketList.vue b/call-client/src/renderer/src/views/TicketList.vue index f8d7f51..90df3bf 100644 --- a/call-client/src/renderer/src/views/TicketList.vue +++ b/call-client/src/renderer/src/views/TicketList.vue @@ -294,7 +294,10 @@ function normalizeRows(raw: unknown[]): Tkt[] { const handleCall = async (row: Tkt): Promise => { window.winControl.windowMinimize() - window.ticketToMain.triggerCall() + window.ticketToMain.triggerCall({ + ticketUid: row.ticketUid, + tktNum: row.tktNum, + }) await window.appLogger.log( 'info', `票池呼叫按钮点击: ticketUid=${row.ticketUid}, tktNum=${row.tktNum}`, @@ -303,7 +306,10 @@ const handleCall = async (row: Tkt): Promise => { const handleReEvaluate = async (row: Tkt): Promise => { window.winControl.windowMinimize() - window.ticketToMain.triggerEvaluate() + window.ticketToMain.triggerEvaluate({ + ticketUid: row.ticketUid, + tktNum: row.tktNum, + }) await window.appLogger.log( 'info', `票池评价按钮点击: ticketUid=${row.ticketUid}, tktNum=${row.tktNum}`,