diff --git a/vue2/apps/web-antd/package.json b/vue2/apps/web-antd/package.json index 3bad547..0357120 100644 --- a/vue2/apps/web-antd/package.json +++ b/vue2/apps/web-antd/package.json @@ -46,6 +46,7 @@ "axios": "catalog:", "dayjs": "catalog:", "echarts": "catalog:", + "ezuikit-flv": "^2.1.0", "js-sha256": "^0.11.1", "markdown-it": "^14.1.0", "markdown-it-table": "^4.1.1", diff --git a/vue2/apps/web-antd/src/api/annual_meeting/exchange.ts b/vue2/apps/web-antd/src/api/annual_meeting/exchange.ts new file mode 100644 index 0000000..7e9912f --- /dev/null +++ b/vue2/apps/web-antd/src/api/annual_meeting/exchange.ts @@ -0,0 +1,24 @@ +import { pyRequestClient } from '#/api/request'; +/** + * 获取所有年会互换数据 + */ +export async function getExchangeListApi() { + return pyRequestClient.get('/am/ExGetList'); +} + +/** + * 重置所有年会互换状态 + */ +export async function resetAllExchangeStatusApi() { + return pyRequestClient.get('/am/ExReset'); +} + +/** + * 重置指定用户的年会互换状态 + * @param targetUserId 指定用户的 UUID + */ +export async function resetUserStatusApi(targetUserId: string) { + return pyRequestClient.put('/am/ExResetTargetStatus', null,{ + params: { target_user_id: targetUserId }, + }); +} diff --git a/vue2/apps/web-antd/src/api/annual_meeting/index.ts b/vue2/apps/web-antd/src/api/annual_meeting/index.ts new file mode 100644 index 0000000..00ccdbf --- /dev/null +++ b/vue2/apps/web-antd/src/api/annual_meeting/index.ts @@ -0,0 +1,2 @@ +export * from './exchange'; +export * from './lottery'; diff --git a/vue2/apps/web-antd/src/api/annual_meeting/lottery.ts b/vue2/apps/web-antd/src/api/annual_meeting/lottery.ts new file mode 100644 index 0000000..54954fd --- /dev/null +++ b/vue2/apps/web-antd/src/api/annual_meeting/lottery.ts @@ -0,0 +1,85 @@ +import { pyRequestClient } from '#/api/request'; + +/** + * 上传 + */ +export async function getLotteryUploadUrl(filename: string) { + return pyRequestClient.get('/am/Lottery/getUploadUrl', { + params: { filename }, + }); +} +/** + * 获取年会礼品列表 + */ +export async function getLotteryList() { + return pyRequestClient.get('/am/Lottery/List'); +} +/** + * 新增礼品(内部自动类型转换) + */ +export async function addLottery( + oss: string, + data: { + is_opened: boolean | number; + name: string; + oss?: string; + remark?: string; + sort: number | string; + }, +) { + delete data.oss_url; + // 自动类型转换 + const payload = { + oss, + ...data, + is_opened: Boolean(data.is_opened), // 0/1/true/false 都能转换成布尔 + sort: Number(data.sort), // "1" / 1 都能转成数字 + }; + + return pyRequestClient.post('/am/Lottery/Add', payload); +} + +/** + * 修改礼品(内部自动类型转换) + */ +export async function updateLottery( + id: string, + oss: string, + data: { + is_opened?: boolean | number; + name?: string; + oss?: string; + remark?: string; + sort?: number | string; + }, +) { + delete data.oss_url; + const payload: any = { id, oss, ...data }; + + // 仅对存在的字段做转换 + if ('is_opened' in payload) payload.is_opened = Boolean(payload.is_opened); + if ('sort' in payload) payload.sort = Number(payload.sort); + + return pyRequestClient.put('/am/Lottery/Update', payload); +} + +/** + * 删除礼品 + */ +export async function deleteLottery(id: string) { + return pyRequestClient.delete('/am/Lottery/Delete', { + params: { id }, + }); +} +/** + * 标记某个奖品为已开启 + */ +export async function openLotteryItem(id: string) { + return pyRequestClient.patch(`/am/Lottery/open/${id}`); +} +/** + * 重置所有奖品为未开启 + */ +export async function resetAllLottery() { + return pyRequestClient.patch('/am/Lottery/resetAll'); +} diff --git a/vue2/apps/web-antd/src/api/index.ts b/vue2/apps/web-antd/src/api/index.ts index 0d69859..6506e6d 100644 --- a/vue2/apps/web-antd/src/api/index.ts +++ b/vue2/apps/web-antd/src/api/index.ts @@ -1,6 +1,8 @@ +export * from './annual_meeting'; export * from './core'; export * from './cv'; export * from './iot'; export * from './llm'; export * from './manager'; export * from './sentinel'; +export * from './ws'; diff --git a/vue2/apps/web-antd/src/api/sentinel/index.ts b/vue2/apps/web-antd/src/api/sentinel/index.ts index 96bb9f9..090587f 100644 --- a/vue2/apps/web-antd/src/api/sentinel/index.ts +++ b/vue2/apps/web-antd/src/api/sentinel/index.ts @@ -1 +1,2 @@ +export * from './monitor'; export * from './record'; diff --git a/vue2/apps/web-antd/src/api/sentinel/monitor.ts b/vue2/apps/web-antd/src/api/sentinel/monitor.ts new file mode 100644 index 0000000..9a95e2c --- /dev/null +++ b/vue2/apps/web-antd/src/api/sentinel/monitor.ts @@ -0,0 +1,19 @@ +import { pyRequestClient } from '#/api/request'; + +/** + * 获取记录列表数据 + */ +export async function getSentinelMonitorPromotionalList() { + return pyRequestClient.get>( + '/iot/sentinel/monitor/promotional/list', + ); +} + +/** + * 获取记录列表数据 + */ +export async function getSentinelMonitorList() { + return pyRequestClient.get>( + '/iot/sentinel/monitor/list', + ); +} diff --git a/vue2/apps/web-antd/src/api/ws.ts b/vue2/apps/web-antd/src/api/ws.ts new file mode 100644 index 0000000..6d9983a --- /dev/null +++ b/vue2/apps/web-antd/src/api/ws.ts @@ -0,0 +1,97 @@ +import { useAccessStore } from '@vben/stores'; + +import { getActivePinia } from 'pinia'; + +function getAccessStore() { + const pinia = getActivePinia(); + if (!pinia) return null; + return useAccessStore(pinia); +} + +type WsOptions = { + maxReconnect?: number; + onClose?: () => void; + onError?: () => void; + onMessage?: (data: any) => void; + onOpen?: () => void; + path: (() => string) | string; + reconnectInterval?: number; +}; +const BASE_WS_URL = 'wss://ai.ronsunny.cn:8090/ai/'; +// const BASE_WS_URL = 'ws://127.0.0.1:13011/'; +export function createAutoReconnectWs(options: WsOptions) { + let ws: null | WebSocket = null; + let reconnectTimer: null | number = null; + let reconnectCount = 0; + + const { + path, + maxReconnect = 5, + reconnectInterval = 2000, + onMessage, + onOpen, + onClose, + onError, + } = options; + + function getUrl() { + const p = typeof path === 'function' ? path() : path; + const token = getAccessStore().accessToken; + return `${BASE_WS_URL}${p}?token=${token}`; + } + + function connect() { + if (ws) { + ws.close(); + ws = null; + } + + ws = new WebSocket(getUrl()); + + ws.addEventListener('open', () => { + reconnectCount = 0; + onOpen?.(); + }); + + ws.addEventListener('message', (e) => { + try { + const data = JSON.parse(e.data); + onMessage?.(data); + } catch { + console.warn('ws message parse error'); + } + }); + + ws.addEventListener('error', () => { + onError?.(); + tryReconnect(); + }); + + ws.addEventListener('close', () => { + onClose?.(); + tryReconnect(); + }); + } + + function tryReconnect() { + if (reconnectTimer) return; + if (reconnectCount >= maxReconnect) return; + + reconnectCount++; + + reconnectTimer = window.setTimeout(() => { + reconnectTimer = null; + connect(); + }, reconnectInterval * reconnectCount); + } + + function close() { + reconnectTimer && clearTimeout(reconnectTimer); + reconnectTimer = null; + reconnectCount = 0; + ws?.close(); + ws = null; + } + + return { connect, close }; +} diff --git a/vue2/apps/web-antd/src/preferences.ts b/vue2/apps/web-antd/src/preferences.ts index 9075274..94679b3 100644 --- a/vue2/apps/web-antd/src/preferences.ts +++ b/vue2/apps/web-antd/src/preferences.ts @@ -9,7 +9,7 @@ export const overridesPreferences = defineOverridesPreferences({ // overrides app: { name: import.meta.env.VITE_APP_TITLE, - layout: 'header-sidebar-nav', + // layout: 'sidebar-mixed-nav', defaultHomePath: '/workspace', // 默认首页路径 enablePreferences: false, // 是否启用偏好设置 enableRefreshToken: true, // 启动刷新token模式 @@ -53,4 +53,10 @@ export const overridesPreferences = defineOverridesPreferences({ middleClickToClose: true, keepAlive: true, }, + header: { + mode: 'static', + }, + logo:{ + enable: true, + } }); diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-control/data.ts b/vue2/apps/web-antd/src/views/annual-meeting/am-control/data.ts new file mode 100644 index 0000000..c51915d --- /dev/null +++ b/vue2/apps/web-antd/src/views/annual-meeting/am-control/data.ts @@ -0,0 +1,172 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { SystemUserApi } from '#/api'; + +import axios from 'axios'; + +import * as api from '#/api'; + +/** + * 新增/修改 + */ +export function useFormSchema(uploadState: { + sizeKb?: number; + uploaded: boolean; + uploadId?: string; + uploading: boolean; +}): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'sort', + label: '顺序号', + componentProps: { + type: 'number', + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'name', + label: '礼品名', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Upload', + fieldName: 'oss_url', // 占位字段 + label: '礼品照', + componentProps: { + maxCount: 1, + accept: 'image/*', // ⚠️ 关键:只允许图片 + customRequest: async ({ file, onSuccess, onError }) => { + try { + uploadState.uploading = true; + uploadState.uploaded = false; + + // 记录文件大小(KiB,保留 2 位小数) + uploadState.sizeKb = Number((file.size / 1024).toFixed(2)); + + // 获取上传地址 + const { uploadUrl, id } = await api.getLotteryUploadUrl(file.name); + + // PUT 上传 + await axios.put(uploadUrl, file, { + headers: { + 'Content-Type': file.type, + }, + }); + + // 记录状态 + uploadState.uploadId = id; + uploadState.uploaded = true; + + onSuccess?.({}, file); + } catch (error) { + uploadState.uploading = false; + uploadState.uploaded = false; + onError?.(error as Error); + } finally { + uploadState.uploading = false; + } + }, + }, + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '已开启', value: 1 }, + { label: '未开启', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 0, + fieldName: 'is_opened', + label: '礼品状态', + }, + { + component: 'Textarea', + fieldName: 'remark', + label: '礼品备注', + componentProps: { + allowClear: true, + }, + }, + ]; +} + +/** + * 列表展示 + * @param onActionClick + * @param onStatusChange + */ +export function useColumns( + onActionClick: OnActionClickFn, + onStatusChange?: (newStatus: any, row: T) => PromiseLike, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + width: 100, + }, + { + field: 'sort', + title: '序号', + width: 150, + }, + { + field: 'name', + title: '奖品名', + width: 150, + }, + { + cellRender: { name: 'CellImage' }, + field: 'oss_url', + title: '奖品照', + width: 150, + }, + { + field: 'is_opened', + title: '当前状态', + width: 100, + cellRender: { + name: 'CellSwitch', + props: { + checkedChildren: '已开启', + unCheckedChildren: '未开启', + }, + attrs: { + beforeChange: onStatusChange, + }, + }, + }, + { + field: 'remark', + title: '备注', + }, + { + field: 'created_at', + title: '录入时间', + width: 150, + }, + { + align: 'center', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: '奖品名称', + onClick: onActionClick, + }, + name: 'CellOperation', + }, + field: 'operation', + fixed: 'right', + title: '操作', + width: 100, + }, + ]; +} diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-control/form.vue b/vue2/apps/web-antd/src/views/annual-meeting/am-control/form.vue new file mode 100644 index 0000000..5221008 --- /dev/null +++ b/vue2/apps/web-antd/src/views/annual-meeting/am-control/form.vue @@ -0,0 +1,145 @@ + + + diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-control/index.vue b/vue2/apps/web-antd/src/views/annual-meeting/am-control/index.vue new file mode 100644 index 0000000..e07d4cb --- /dev/null +++ b/vue2/apps/web-antd/src/views/annual-meeting/am-control/index.vue @@ -0,0 +1,186 @@ + + diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-exchange/index.vue b/vue2/apps/web-antd/src/views/annual-meeting/am-exchange/index.vue new file mode 100644 index 0000000..3097b51 --- /dev/null +++ b/vue2/apps/web-antd/src/views/annual-meeting/am-exchange/index.vue @@ -0,0 +1,510 @@ + + + + + diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/bg.jpeg b/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/bg.jpeg new file mode 100644 index 0000000..43fdf76 Binary files /dev/null and b/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/bg.jpeg differ diff --git a/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/index.vue b/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/index.vue new file mode 100644 index 0000000..0b74df1 --- /dev/null +++ b/vue2/apps/web-antd/src/views/annual-meeting/am-lottery/index.vue @@ -0,0 +1,703 @@ + + + + + diff --git a/vue2/apps/web-antd/src/views/cv/sca-show/index.vue b/vue2/apps/web-antd/src/views/cv/sca-show/index.vue new file mode 100644 index 0000000..7031374 --- /dev/null +++ b/vue2/apps/web-antd/src/views/cv/sca-show/index.vue @@ -0,0 +1,321 @@ + + + + + diff --git a/vue2/apps/web-antd/src/views/cv/ysa/index.vue b/vue2/apps/web-antd/src/views/cv/ysa/index.vue index 8e4f431..98346cc 100644 --- a/vue2/apps/web-antd/src/views/cv/ysa/index.vue +++ b/vue2/apps/web-antd/src/views/cv/ysa/index.vue @@ -264,6 +264,7 @@ function changePage(newPage) {