feat: add PopupAd and AdminPopupAd interfaces with CRUD operations

- Introduced PopupAd and AdminPopupAd interfaces in common.ts.
- Implemented encoding, decoding, and JSON conversion methods for both PopupAd and AdminPopupAd.
- Added new RPC methods for managing PopupAds in admin.ts and me.ts, including list, create, update, and delete functionalities.
- Integrated PopupAdsClient in grpcClient.ts for gRPC communication.
- Updated auth store to handle real-time notifications for user-specific topics.
- Modified tsconfig.json to include auto-imports and components type definitions.
This commit is contained in:
2026-03-29 06:42:37 +00:00
parent 43702e8bf7
commit 8515498ade
31 changed files with 3905 additions and 78 deletions

View File

@@ -11,8 +11,8 @@ import DomainsDnsEmbedCode from './components/DomainsDnsEmbedCode.vue';
import DomainsDnsNotices from './components/DomainsDnsNotices.vue';
import DomainsDnsTable from './components/DomainsDnsTable.vue';
import DomainsDnsToolbar from './components/DomainsDnsToolbar.vue';
import { mapDomainItem, normalizeDomainInput } from './helpers';
import type { DomainItem } from './types';
import { normalizeDomainInput } from './helpers';
import type { Domain } from '@/server/api/proto/app/v1/common';
const toast = useAppToast();
const confirm = useAppConfirm();
@@ -27,7 +27,7 @@ const { data: domainsSnapshot, error, isPending, refetch } = useQuery({
key: () => ['settings', 'domains'],
query: async () => {
const response = await rpcClient.listDomains();
return (response.domains || []).map(mapDomainItem);
return (response.domains || []);
},
});
@@ -126,16 +126,16 @@ const handleAddDomain = async () => {
}
};
const handleRemoveDomain = (domain: DomainItem) => {
const handleRemoveDomain = (domain: Domain) => {
confirm.require({
message: t('settings.domainsDns.confirm.removeMessage', { domain: domain.name }),
header: t('settings.domainsDns.confirm.removeHeader'),
acceptLabel: t('settings.domainsDns.confirm.removeAccept'),
rejectLabel: t('settings.domainsDns.confirm.removeReject'),
accept: async () => {
removingId.value = domain.id;
removingId.value = domain.id!;
try {
await rpcClient.deleteDomain({ id: domain.id });
await rpcClient.deleteDomain({ id: domain.id! });
await refetch();
toast.add({
severity: 'info',

View File

@@ -3,31 +3,31 @@ import LinkIcon from '@/components/icons/LinkIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import AppButton from '@/components/ui/AppButton.vue';
import BaseTable from '@/components/ui/BaseTable.vue';
import { formatDate } from '@/lib/utils';
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
import type { Domain } from '@/server/api/proto/app/v1/common';
import type { ColumnDef } from '@tanstack/vue-table';
import { useTranslation } from 'i18next-vue';
import { computed, h } from 'vue';
import type { DomainItem } from '../types';
const props = defineProps<{
domains: DomainItem[];
domains: Domain[];
isInitialLoading: boolean;
adding: boolean;
removingId: string | null;
}>();
const emit = defineEmits<{
(e: 'remove', domain: DomainItem): void;
(e: 'remove', domain: Domain): void;
}>();
const { t } = useTranslation();
const columns = computed<ColumnDef<DomainItem>[]>(() => [
const columns = computed<ColumnDef<Domain>[]>(() => [
{
id: 'domain',
header: t('settings.domainsDns.table.domain'),
accessorFn: row => row.name,
cell: ({ row }) => h('div', { class: 'flex items-center gap-2' }, [
cell: ({ row, getValue }) => h('div', { class: 'flex items-center gap-2' }, [
h(LinkIcon, { class: 'h-4 w-4 text-foreground/40' }),
h('span', { class: 'text-sm font-medium text-foreground' }, row.original.name),
]),
@@ -39,8 +39,8 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
{
id: 'addedAt',
header: t('settings.domainsDns.table.addedDate'),
accessorFn: row => row.addedAt,
cell: ({ row }) => h('span', { class: 'text-sm text-foreground/60' }, row.original.addedAt),
accessorFn: row => formatDate(row.createdAt),
cell: ({ getValue }) => h('span', { class: 'text-sm text-foreground/60' }, getValue<string>()),
meta: {
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
cellClass: 'px-6 py-3',
@@ -58,10 +58,7 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
}, {
icon: () => h(TrashIcon, { class: 'h-4 w-4 text-danger' }),
}),
meta: {
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
cellClass: 'px-6 py-3 text-right',
},
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center', cellClass: 'px-6 py-3 text-center' },
},
]);
</script>
@@ -73,7 +70,7 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
v-else
:data="domains"
:columns="columns"
:get-row-id="(row) => row.id"
:get-row-id="(row) => row.id!"
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
tableClass="w-full"
headerRowClass="bg-muted/30"

View File

@@ -1,5 +1,3 @@
import type { DomainApiItem, DomainItem } from './types';
export const normalizeDomainInput = (value: string) => value
.trim()
.toLowerCase()
@@ -7,19 +5,19 @@ export const normalizeDomainInput = (value: string) => value
.replace(/^www\./, '')
.replace(/\/$/, '');
export const formatDate = (value?: string) => {
if (!value) return '-';
// export const formatDate = (value?: string) => {
// if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value.split('T')[0] || value;
}
// const date = new Date(value);
// if (Number.isNaN(date.getTime())) {
// return value.split('T')[0] || value;
// }
return date.toISOString().split('T')[0];
};
// return date.toISOString().split('T')[0];
// };
export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
name: item.name || '',
addedAt: formatDate(item.created_at),
});
// export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
// id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
// name: item.name || '',
// addedAt: formatDate(item.created_at),
// });

View File

@@ -1,11 +0,0 @@
export type DomainApiItem = {
id?: string;
name?: string;
created_at?: string;
};
export type DomainItem = {
id: string;
name: string;
addedAt: string;
};