自动叫号,转移
parent
25b4217969
commit
603e07977f
@ -0,0 +1,5 @@
|
||||
export interface TransferBusiness {
|
||||
businessUid: number;
|
||||
businessName: string;
|
||||
businessCode?: string;
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
import { getAllConfig, mergeConfig } from "../host/config";
|
||||
import type { CallStatus } from "../types/action";
|
||||
|
||||
/** 写入 `config.json`,登录重置 session 后仍可恢复 */
|
||||
export const INTERRUPTED_TICKET_CONFIG_KEY = "interrupted_ticket_snapshot";
|
||||
|
||||
/** 呼叫/开始之后可恢复的流程状态 */
|
||||
export type InterruptedFlowStatus = Extract<
|
||||
CallStatus,
|
||||
"calling" | "working" | "paused"
|
||||
>;
|
||||
|
||||
export interface InterruptedTicketSnapshot {
|
||||
ticketUid: number;
|
||||
ticketNo: string;
|
||||
flowStatus: InterruptedFlowStatus;
|
||||
windowUid: number;
|
||||
empUid: number;
|
||||
pauseReason?: string;
|
||||
savedAt: number;
|
||||
}
|
||||
|
||||
function parseFlowStatus(value: unknown): InterruptedFlowStatus | null {
|
||||
if (value === "calling" || value === "working" || value === "paused") {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseSnapshot(raw: unknown): InterruptedTicketSnapshot | null {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
const ticketUid = Number(record.ticketUid);
|
||||
const windowUid = Number(record.windowUid);
|
||||
const empUid = Number(record.empUid);
|
||||
const flowStatus = parseFlowStatus(record.flowStatus);
|
||||
if (
|
||||
!Number.isFinite(ticketUid) ||
|
||||
ticketUid <= 0 ||
|
||||
!flowStatus ||
|
||||
!Number.isFinite(windowUid) ||
|
||||
windowUid <= 0 ||
|
||||
!Number.isFinite(empUid) ||
|
||||
empUid <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const ticketNo = String(record.ticketNo ?? "").trim();
|
||||
const pauseReason =
|
||||
typeof record.pauseReason === "string" && record.pauseReason.trim() !== ""
|
||||
? record.pauseReason.trim()
|
||||
: undefined;
|
||||
const savedAt = Number(record.savedAt);
|
||||
return {
|
||||
ticketUid,
|
||||
ticketNo,
|
||||
flowStatus,
|
||||
windowUid,
|
||||
empUid,
|
||||
pauseReason,
|
||||
savedAt: Number.isFinite(savedAt) && savedAt > 0 ? savedAt : Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 读取异常中断票号缓存 */
|
||||
export async function readInterruptedTicketSnapshot(): Promise<InterruptedTicketSnapshot | null> {
|
||||
try {
|
||||
const config = await getAllConfig();
|
||||
return parseSnapshot(config[INTERRUPTED_TICKET_CONFIG_KEY]);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 写入异常中断票号缓存 */
|
||||
export async function saveInterruptedTicketSnapshot(
|
||||
snapshot: InterruptedTicketSnapshot,
|
||||
): Promise<void> {
|
||||
await mergeConfig({
|
||||
[INTERRUPTED_TICKET_CONFIG_KEY]: {
|
||||
...snapshot,
|
||||
savedAt: snapshot.savedAt > 0 ? snapshot.savedAt : Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 清除异常中断票号缓存 */
|
||||
export async function clearInterruptedTicketSnapshot(): Promise<void> {
|
||||
await mergeConfig({
|
||||
[INTERRUPTED_TICKET_CONFIG_KEY]: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function interruptedFlowStatusLabel(
|
||||
status: InterruptedFlowStatus,
|
||||
): string {
|
||||
switch (status) {
|
||||
case "calling":
|
||||
return "呼叫中";
|
||||
case "working":
|
||||
return "办理中";
|
||||
case "paused":
|
||||
return "已暂停";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
import type { TransferRequest } from "../types/action";
|
||||
import type { SessionState } from "../host/types";
|
||||
import type { TransferBusiness } from "../types/transfer";
|
||||
import type { ServiceWindow, WindowResponse } from "../types/window";
|
||||
|
||||
function extractArray(raw: unknown): unknown[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw;
|
||||
}
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return [];
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
for (const key of [
|
||||
"businesses",
|
||||
"businessList",
|
||||
"list",
|
||||
"items",
|
||||
"records",
|
||||
"data",
|
||||
]) {
|
||||
const value = record[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeBusinessItem(item: unknown): TransferBusiness | null {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = item as Record<string, unknown>;
|
||||
const businessUid = Number(
|
||||
record.businessUid ?? record.bizUid ?? record.uid ?? record.id ?? -1,
|
||||
);
|
||||
if (!Number.isFinite(businessUid) || businessUid <= 0) {
|
||||
return null;
|
||||
}
|
||||
const businessName = String(
|
||||
record.businessName ??
|
||||
record.bizName ??
|
||||
record.name ??
|
||||
record.label ??
|
||||
record.title ??
|
||||
"",
|
||||
).trim();
|
||||
const businessCode = String(
|
||||
record.businessCode ?? record.bizCode ?? record.code ?? "",
|
||||
).trim();
|
||||
return {
|
||||
businessUid,
|
||||
businessName: businessName || `业务 ${businessUid}`,
|
||||
businessCode: businessCode || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析 business-list 接口返回(兼容多种字段命名) */
|
||||
export function normalizeBusinessList(raw: unknown): TransferBusiness[] {
|
||||
return extractArray(raw)
|
||||
.map((item) => normalizeBusinessItem(item))
|
||||
.filter((item): item is TransferBusiness => item !== null);
|
||||
}
|
||||
|
||||
/** 转移目标窗口列表(排除当前窗口) */
|
||||
export function normalizeTransferWindows(
|
||||
raw: WindowResponse | unknown,
|
||||
currentWindowUid: number,
|
||||
): ServiceWindow[] {
|
||||
const windows = Array.isArray((raw as WindowResponse)?.windows)
|
||||
? (raw as WindowResponse).windows
|
||||
: [];
|
||||
return windows.filter(
|
||||
(win) =>
|
||||
Number.isFinite(win.windowUid) &&
|
||||
win.windowUid > 0 &&
|
||||
win.windowUid !== currentWindowUid,
|
||||
);
|
||||
}
|
||||
|
||||
export type TransferTarget =
|
||||
| { kind: "window"; windowUid: number }
|
||||
| { kind: "business"; business: TransferBusiness };
|
||||
|
||||
/** 组装转移接口请求体(窗口与业务二选一) */
|
||||
export function buildTransferRequest(
|
||||
session: SessionState,
|
||||
ticketUid: number,
|
||||
target: TransferTarget,
|
||||
): TransferRequest {
|
||||
const base: TransferRequest = {
|
||||
windowUid: Number(session.winUid ?? 0),
|
||||
empUid: Number(session.empUid ?? 0),
|
||||
ticketUid,
|
||||
targetWindowUid: null,
|
||||
targetBusinessUid: null,
|
||||
resumeToken: "",
|
||||
targetPosition: 0,
|
||||
rank: 0,
|
||||
rankUserName: "",
|
||||
rankUserPhone: "",
|
||||
idCard: "",
|
||||
phone: "",
|
||||
serviceUrl: String(session.ziyunServiceUrl ?? "").trim(),
|
||||
forward: true,
|
||||
read: true,
|
||||
customText: "",
|
||||
pauseReason: "",
|
||||
driver: "",
|
||||
clients: [],
|
||||
};
|
||||
if (target.kind === "window") {
|
||||
return {
|
||||
...base,
|
||||
targetWindowUid: target.windowUid,
|
||||
targetBusinessUid: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
targetWindowUid: null,
|
||||
targetBusinessUid: target.business.businessUid,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,504 @@
|
||||
<template>
|
||||
<div class="transfer-container">
|
||||
<div class="transfer-header">
|
||||
<div class="transfer-header-title" data-tauri-drag-region @dblclick.prevent.stop>
|
||||
票号转移
|
||||
</div>
|
||||
<div class="transfer-header-actions">
|
||||
<button class="control-button" type="button" @mousedown.stop @click="handleMinimizeClick">
|
||||
<el-icon class="control-icon">
|
||||
<component :is="Minus" />
|
||||
</el-icon>
|
||||
</button>
|
||||
<button
|
||||
class="control-button"
|
||||
type="button"
|
||||
@mousedown.stop
|
||||
@dblclick.prevent.stop
|
||||
@click="handleCloseClick"
|
||||
>
|
||||
<el-icon class="control-icon">
|
||||
<component :is="Close" />
|
||||
</el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="transfer-body">
|
||||
<p v-if="loadError" class="transfer-error">{{ loadError }}</p>
|
||||
<p v-else class="transfer-hint">目标窗口与目标业务只能二选一</p>
|
||||
<div v-if="!loadError" class="transfer-columns">
|
||||
<section class="transfer-column">
|
||||
<h3 class="transfer-column-title">目标窗口</h3>
|
||||
<el-radio-group
|
||||
v-if="windows.length > 0"
|
||||
v-model="selectedWindowUid"
|
||||
class="transfer-radio-group"
|
||||
@change="onWindowSelectionChange"
|
||||
>
|
||||
<el-radio
|
||||
v-for="win in windows"
|
||||
:key="win.windowUid"
|
||||
:value="win.windowUid"
|
||||
class="transfer-radio"
|
||||
>
|
||||
<span class="transfer-radio-label">{{ win.windowName }}</span>
|
||||
<span v-if="win.windowCode" class="transfer-radio-sub">
|
||||
{{ win.windowCode }}
|
||||
</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-empty v-else description="暂无可选窗口" :image-size="56" />
|
||||
</section>
|
||||
|
||||
<section class="transfer-column">
|
||||
<h3 class="transfer-column-title">目标业务</h3>
|
||||
<el-radio-group
|
||||
v-if="businesses.length > 0"
|
||||
v-model="selectedBusinessUid"
|
||||
class="transfer-radio-group"
|
||||
@change="onBusinessSelectionChange"
|
||||
>
|
||||
<el-radio
|
||||
v-for="biz in businesses"
|
||||
:key="biz.businessUid"
|
||||
:value="biz.businessUid"
|
||||
class="transfer-radio"
|
||||
>
|
||||
<span class="transfer-radio-label">{{ biz.businessName }}</span>
|
||||
<span v-if="biz.businessCode" class="transfer-radio-sub">
|
||||
{{ biz.businessCode }}
|
||||
</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-empty v-else description="暂无可选业务" :image-size="56" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transfer-footer">
|
||||
<el-button @click="handleCloseClick">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="loading || Boolean(loadError) || !hasTransferTarget"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认转移
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, Minus } from "@element-plus/icons-vue";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { ElButton, ElEmpty, ElRadio, ElRadioGroup } from "element-plus";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { api } from "../api";
|
||||
import { showErrorNative, showWarningNative } from "../host/dialog";
|
||||
import { emitTransferDone, listenTransferOpen } from "../host/events";
|
||||
import { log } from "../host/logger";
|
||||
import { getSession, setSession } from "../host/session";
|
||||
import type { SessionState } from "../host/types";
|
||||
import type { TransferBusiness } from "../types/transfer";
|
||||
import type { ServiceWindow } from "../types/window";
|
||||
import { closeTransferWindow, focusNamedWindow, minimizeWindow } from "../host/window";
|
||||
import {
|
||||
buildTransferRequest,
|
||||
normalizeBusinessList,
|
||||
normalizeTransferWindows,
|
||||
type TransferTarget,
|
||||
} from "../utils/transfer";
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const loadError = ref("");
|
||||
const ticketUid = ref(-1);
|
||||
const currentWindowUid = ref(0);
|
||||
const windows = ref<ServiceWindow[]>([]);
|
||||
const businesses = ref<TransferBusiness[]>([]);
|
||||
const selectedWindowUid = ref<number | undefined>(undefined);
|
||||
const selectedBusinessUid = ref<number | undefined>(undefined);
|
||||
|
||||
const hasTransferTarget = computed(() => {
|
||||
const hasWindow = selectedWindowUid.value !== undefined;
|
||||
const hasBusiness = selectedBusinessUid.value !== undefined;
|
||||
return hasWindow !== hasBusiness;
|
||||
});
|
||||
|
||||
function onWindowSelectionChange(): void {
|
||||
if (selectedWindowUid.value !== undefined) {
|
||||
selectedBusinessUid.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onBusinessSelectionChange(): void {
|
||||
if (selectedBusinessUid.value !== undefined) {
|
||||
selectedWindowUid.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTransferTarget(): TransferTarget | null {
|
||||
if (
|
||||
selectedWindowUid.value !== undefined &&
|
||||
selectedBusinessUid.value === undefined
|
||||
) {
|
||||
const windowUid = selectedWindowUid.value;
|
||||
if (windows.value.some((item) => item.windowUid === windowUid)) {
|
||||
return { kind: "window", windowUid };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
selectedBusinessUid.value !== undefined &&
|
||||
selectedWindowUid.value === undefined
|
||||
) {
|
||||
const business = businesses.value.find(
|
||||
(item) => item.businessUid === selectedBusinessUid.value,
|
||||
);
|
||||
if (business) {
|
||||
return { kind: "business", business };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let unlistenTransferOpen: (() => void) | null = null;
|
||||
let unlistenWindowFocus: (() => void) | null = null;
|
||||
|
||||
function parseOptionalNumber(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTicketUidFromSession(
|
||||
session: SessionState,
|
||||
explicit?: number,
|
||||
): number {
|
||||
if (typeof explicit === "number" && explicit > 0) {
|
||||
return explicit;
|
||||
}
|
||||
const fromTransfer = parseOptionalNumber(session.transferTicketUid);
|
||||
if (fromTransfer !== null && fromTransfer > 0) {
|
||||
return fromTransfer;
|
||||
}
|
||||
const fromActive = parseOptionalNumber(session.activeTicketUid);
|
||||
if (fromActive !== null && fromActive > 0) {
|
||||
return fromActive;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getActionData(res: unknown): Record<string, unknown> {
|
||||
const result = (res ?? {}) as { data?: Record<string, unknown> } & Record<string, unknown>;
|
||||
return result.data && typeof result.data === "object" ? result.data : result;
|
||||
}
|
||||
|
||||
function isActionSuccess(res: unknown): boolean {
|
||||
return getActionData(res).success === true;
|
||||
}
|
||||
|
||||
function getActionMessage(res: unknown): string {
|
||||
return String(getActionData(res).message ?? "");
|
||||
}
|
||||
|
||||
function getActionTicketNo(res: unknown): string {
|
||||
return String(getActionData(res).ticketNo ?? "");
|
||||
}
|
||||
|
||||
async function loadTransferOptions(explicitTicketUid?: number): Promise<void> {
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
windows.value = [];
|
||||
businesses.value = [];
|
||||
selectedWindowUid.value = undefined;
|
||||
selectedBusinessUid.value = undefined;
|
||||
|
||||
try {
|
||||
const session = await getSession();
|
||||
currentWindowUid.value = Number(session.winUid ?? 0);
|
||||
ticketUid.value = resolveTicketUidFromSession(session, explicitTicketUid);
|
||||
if (ticketUid.value <= 0) {
|
||||
loadError.value =
|
||||
"未获取到待转移票号,请先在主窗口完成呼叫并开始办理后再打开转移";
|
||||
await log(
|
||||
"warn",
|
||||
`转移窗口: 无有效 ticketUid transferTicketUid=${String(session.transferTicketUid)} activeTicketUid=${String(session.activeTicketUid)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [winRes, bizRes] = await Promise.all([
|
||||
api.window.list(),
|
||||
api.action.businessList(),
|
||||
]);
|
||||
windows.value = normalizeTransferWindows(winRes, currentWindowUid.value);
|
||||
businesses.value = normalizeBusinessList(bizRes);
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : String(error);
|
||||
await log(
|
||||
"error",
|
||||
`转移窗口加载选项失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMinimizeClick(): Promise<void> {
|
||||
try {
|
||||
await minimizeWindow();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await showErrorNative(message || "最小化失败", "票号转移");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCloseClick(): Promise<void> {
|
||||
try {
|
||||
await closeTransferWindow();
|
||||
await focusNamedWindow("main");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await showErrorNative(message || "关闭失败", "票号转移");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirm(): Promise<void> {
|
||||
const target = resolveTransferTarget();
|
||||
if (!target) {
|
||||
await showWarningNative(
|
||||
"请选择目标窗口或目标业务(只能二选一)",
|
||||
"票号转移",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const session = await getSession();
|
||||
const res = await api.action.transfer(
|
||||
buildTransferRequest(session, ticketUid.value, target),
|
||||
);
|
||||
|
||||
if (isActionSuccess(res)) {
|
||||
await setSession({
|
||||
...session,
|
||||
activeTicketUid: null,
|
||||
transferTicketUid: null,
|
||||
});
|
||||
const targetWindowName =
|
||||
target.kind === "window"
|
||||
? windows.value.find((item) => item.windowUid === target.windowUid)
|
||||
?.windowName
|
||||
: undefined;
|
||||
const businessName =
|
||||
target.kind === "business" ? target.business.businessName : undefined;
|
||||
await emitTransferDone({
|
||||
success: true,
|
||||
ticketNo: getActionTicketNo(res),
|
||||
targetWindowName,
|
||||
businessName,
|
||||
message: getActionMessage(res),
|
||||
});
|
||||
await closeTransferWindow();
|
||||
await focusNamedWindow("main");
|
||||
return;
|
||||
}
|
||||
|
||||
await showWarningNative(
|
||||
getActionMessage(res) || "转移未成功",
|
||||
"票号转移",
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await showErrorNative(message || "转移失败", "票号转移");
|
||||
await log("error", `转移提交失败: ${message}`);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
unlistenTransferOpen = await listenTransferOpen((payload) => {
|
||||
void loadTransferOptions(payload.ticketUid);
|
||||
});
|
||||
unlistenWindowFocus = await appWindow.onFocusChanged(({ payload: focused }) => {
|
||||
if (focused) {
|
||||
void loadTransferOptions();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await log(
|
||||
"error",
|
||||
`转移窗口订阅事件失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
void loadTransferOptions();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unlistenTransferOpen) {
|
||||
unlistenTransferOpen();
|
||||
}
|
||||
if (unlistenWindowFocus) {
|
||||
unlistenWindowFocus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.transfer-container {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.transfer-header {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, #004d99 0%, #003b7a 100%);
|
||||
color: #fff;
|
||||
padding: 0 6px 0 12px;
|
||||
}
|
||||
|
||||
.transfer-header-title {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.transfer-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.transfer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.transfer-error {
|
||||
margin: 0;
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.transfer-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.transfer-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.transfer-column {
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transfer-column-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.transfer-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.transfer-radio {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-right: 0;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.transfer-radio-label {
|
||||
display: block;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.transfer-radio-sub {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.transfer-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue