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}`,