Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions ui/components/AssetContextMenu/contextMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { AssetItem, PermedProtocol } from "~/types/index";

import { storeToRefs } from "pinia";
import { useUserInfoStore } from "~/store/modules/userInfo";

interface Props {
Expand All @@ -22,6 +22,8 @@ const emits = defineEmits<{

const { t } = useI18n();
const { handleAssetConnection, displayUser, handleAssetFavorite, handleAssetUnfavorite } = useAssetAction();
const userInfoStore = useUserInfoStore();
const { currentConnectionInfoMap } = storeToRefs(userInfoStore);

interface MenuItem {
value?: string;
Expand All @@ -33,9 +35,28 @@ interface MenuItem {

const isFavorited = computed(() => !!props.asset.isFavorite);

const resolveProtocols = (asset: AssetItem) => {
const candidateProtocols: string[] = [];

(asset.permedProtocols || []).forEach((p: PermedProtocol | undefined) => {
if (p?.name) candidateProtocols.push(p.name);
});

const saved = currentConnectionInfoMap.value[asset.id];

(saved?.availableProtocols || []).forEach((name) => {
if (name) candidateProtocols.push(name);
});

if (saved?.protocol) {
candidateProtocols.push(saved.protocol);
}

return Array.from(new Set(candidateProtocols.filter((name) => typeof name === "string" && name.length > 0)));
};

const menuItems = computed((): MenuItem[] => {
const protocols = (props.asset.permedProtocols || []).map((p: PermedProtocol) => p.name);
const uniqueProtocols = Array.from(new Set(protocols));
const uniqueProtocols = resolveProtocols(props.asset);

const baseItems: MenuItem[] = [
{
Expand Down
3 changes: 2 additions & 1 deletion ui/components/BasePage/basePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ const handleConnectAsset = async (asset: AssetItem) => {
manualUsername: saved!.manualUsername || "",
manualPassword: saved!.manualPassword || "",
dynamicPassword: saved!.dynamicPassword || "",
rememberSecret: !!saved!.rememberSecret
rememberSecret: !!saved!.rememberSecret,
availableProtocols: saved!.availableProtocols || []
});
}

Expand Down
62 changes: 40 additions & 22 deletions ui/components/Card/TableCard/tableCard.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script setup lang="ts">
import type { AssetItem } from "~/types";
import type { AssetItem, PermedProtocol } from "~/types";
import type { TableColumn } from "@nuxt/ui";
import { UCheckbox, UButton } from "#components";

import { h, resolveComponent } from "vue";
import { storeToRefs } from "pinia";
import { useUserInfoStore } from "~/store/modules/userInfo";

interface MenuItem {
icon: string;
Expand All @@ -13,6 +15,7 @@ interface MenuItem {
}

const UButton = resolveComponent("UButton");
const UCheckbox = resolveComponent("UCheckbox");
const UFieldGroup = resolveComponent("UFieldGroup");
const UDropdownMenu = resolveComponent("UDropdownMenu");

Expand All @@ -36,6 +39,8 @@ const {
handleAssetUnfavorite,
handleAssetConnection
} = useAssetAction();
const userInfoStore = useUserInfoStore();
const { currentConnectionInfoMap } = storeToRefs(userInfoStore);

const renameValue = ref("");
const contextMenuVisible = ref(false);
Expand All @@ -45,10 +50,28 @@ const contextMenuAsset = ref<AssetItem | null>(null);
const renameInputEl = ref<HTMLInputElement | null>(null);
const actionMenuOpen = reactive<Record<string, boolean>>({});

const resolveProtocols = (asset: AssetItem) => {
const candidates: string[] = [];

(asset.permedProtocols || []).forEach((p: PermedProtocol | undefined) => {
if (p?.name) candidates.push(p.name);
});

const saved = currentConnectionInfoMap.value[asset.id];
(saved?.availableProtocols || []).forEach((name) => {
if (name) candidates.push(name);
});

if (saved?.protocol) {
candidates.push(saved.protocol);
}

return Array.from(new Set(candidates.filter((name) => typeof name === "string" && name.length > 0)));
};

const buildMenuItems = computed(() => {
return (asset: AssetItem): MenuItem[] => {
const protocols = (asset.permedProtocols || []).map((p: any) => p.name);
const uniqueProtocols = Array.from(new Set(protocols));
const uniqueProtocols = resolveProtocols(asset);

const items: MenuItem[] = [
{
Expand Down Expand Up @@ -162,21 +185,18 @@ function cancelRename() {

const columns: TableColumn<AssetItem>[] = [
{
id: 'select',
id: "select",
header: ({ table }) =>
h(UCheckbox, {
modelValue: table.getIsSomePageRowsSelected()
? 'indeterminate'
: table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
modelValue: table.getIsSomePageRowsSelected() ? "indeterminate" : table.getIsAllPageRowsSelected(),
"onUpdate:modelValue": (value: boolean | "indeterminate") => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all"
}),
cell: ({ row }) =>
h(UCheckbox, {
modelValue: row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
"onUpdate:modelValue": (value: boolean | "indeterminate") => row.toggleSelected(!!value),
"aria-label": "Select row"
}),
meta: { class: { th: "w-[50px]", td: "w-[50px]" } }
},
Expand Down Expand Up @@ -251,15 +271,13 @@ const columns: TableColumn<AssetItem>[] = [
if (!protocolText || protocolText === "-") {
return h("div", { class: "truncate" }, "-");
}
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]

return h(UButton, { size: 'xs', class: 'rounded-sm', variant: 'subtle', color: 'primary' }, () =>
protocolText
)
const color = {
paid: "success" as const,
failed: "error" as const,
refunded: "neutral" as const
}[row.getValue("status") as string];

return h(UButton, { size: "xs", class: "rounded-sm", variant: "subtle", color: "primary" }, () => protocolText);
},
meta: { class: { th: "w-[150px]", td: "w-[150px]" } }
},
Expand Down
9 changes: 8 additions & 1 deletion ui/components/ConnectionEditor/connectionEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ const initDraft = (asset: AssetItem) => {
/**
* @description 拼凑连接信息
*/
const normalizeProtocols = () => {
return (currentAsset.value?.permedProtocols || [])
.map((p) => (p?.name ? p.name.trim() : ""))
.filter((name) => name.length > 0);
};

const buildConnectionInfo = () => {
let accountMode: "hosted" | "dynamic" | "manual" = "hosted";
let normalizedAccount = draftAccount.value || "";
Expand Down Expand Up @@ -102,7 +108,8 @@ const buildConnectionInfo = () => {
manualUsername: draftManualUsername.value || "",
manualPassword: draftManualPassword.value || "",
dynamicPassword: draftDynamicPassword.value || "",
rememberSecret: !!draftRememberSecret.value
rememberSecret: !!draftRememberSecret.value,
availableProtocols: normalizeProtocols()
};
};

Expand Down
33 changes: 24 additions & 9 deletions ui/composables/useAssetConnection.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { AssetItem } from "~/types/index";
import type { AssetItem, ConnectionInfo as StoredConnectionInfo } from "~/types/index";
import { useUserInfoStore } from "~/store/modules/userInfo";

type ConnectionInfo = {
type ConnectionFormInfo = {
protocol: string;
account: string;
accountId?: string;
accountMode: "hosted" | "dynamic" | "manual";
manualUsername: string;
manualPassword: string;
dynamicPassword: string;
rememberSecret: boolean;

accountId?: string;
availableProtocols?: string[];
accountMode: "hosted" | "dynamic" | "manual";
};

export function useAssetConnection() {
Expand Down Expand Up @@ -44,9 +46,19 @@ export function useAssetConnection() {
/**
* 仅保存连接信息(不触发连接)
*/
const saveConnectionInfo = (asset: AssetItem, connectionInfo: ConnectionInfo) => {
const saveConnectionInfo = (asset: AssetItem, connectionInfo: ConnectionFormInfo) => {
let resolvedAccountId: string | undefined = connectionInfo.accountId;

const candidateProtocols = (
connectionInfo.availableProtocols && connectionInfo.availableProtocols.length > 0
? connectionInfo.availableProtocols
: (asset.permedProtocols || []).map((p) => (typeof p?.name === "string" ? p.name.trim() : ""))
) as string[];

const availableProtocols = Array.from(
new Set(candidateProtocols.map((name) => (typeof name === "string" ? name.trim() : "")).filter((name) => name))
);

if (connectionInfo.accountMode === "hosted" && !resolvedAccountId) {
const accs = asset.permedAccounts || [];
const matched = accs.find(
Expand All @@ -59,22 +71,25 @@ export function useAssetConnection() {
resolvedAccountId = matched?.id;
}

userInfoStore.setConnectionInfoForAsset(asset.id, {
const payload: StoredConnectionInfo = {
protocol: connectionInfo.protocol,
username: connectionInfo.account,
accountId: resolvedAccountId,
accountMode: connectionInfo.accountMode,
manualUsername: connectionInfo.rememberSecret ? connectionInfo.manualUsername : "",
manualPassword: connectionInfo.rememberSecret ? connectionInfo.manualPassword : "",
dynamicPassword: connectionInfo.rememberSecret ? connectionInfo.dynamicPassword : "",
rememberSecret: connectionInfo.rememberSecret
});
rememberSecret: connectionInfo.rememberSecret,
availableProtocols
};

userInfoStore.setConnectionInfoForAsset(asset.id, payload);
};

/**
* 处理连接确认(从模态框)
*/
const confirmConnection = (asset: AssetItem, connectionInfo: ConnectionInfo) => {
const confirmConnection = (asset: AssetItem, connectionInfo: ConnectionFormInfo) => {
saveConnectionInfo(asset, connectionInfo);

handleAssetConnection(connectionInfo.account, asset.id, connectionInfo.protocol, asset.permedAccounts!, undefined, {
Expand Down
19 changes: 17 additions & 2 deletions ui/store/modules/userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,23 @@ export const useUserInfoStore = defineStore(
siteData.connectionInfoMap = {};
}

siteData.connectionInfoMap[assetId] = connectionInfo;
currentConnectionInfoMap.value = siteData.connectionInfoMap;
const existing = siteData.connectionInfoMap[assetId];
const incomingProtocols = (connectionInfo.availableProtocols || [])
.map((p) => (typeof p === "string" ? p.trim() : ""))
.filter((p) => p.length > 0);

const mergedProtocols =
incomingProtocols.length > 0
? Array.from(new Set(incomingProtocols))
: existing?.availableProtocols;

siteData.connectionInfoMap[assetId] = {
...(existing || {}),
...connectionInfo,
...(mergedProtocols && mergedProtocols.length > 0 ? { availableProtocols: mergedProtocols } : {})
};

currentConnectionInfoMap.value = { ...siteData.connectionInfoMap };
};

const applyLanguageToAll = (lang: LangType) => {
Expand Down
1 change: 1 addition & 0 deletions ui/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export interface ConnectionInfo {
manualPassword?: string;
rememberSecret?: boolean;
dynamicPassword?: string;
availableProtocols?: string[];
}

export interface RdpGraphics {
Expand Down