chore: 并入 iotlcm-onelink-mock 历史到 onelink-mock/(原提交 f398595)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
*.md
|
||||
.env
|
||||
.env.*
|
||||
.vscode
|
||||
.idea
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
|
||||
|
||||
RUN corepack enable \
|
||||
&& corepack prepare pnpm@11.0.9 --activate
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
|
||||
RUN pnpm config set registry https://registry.npmmirror.com \
|
||||
&& pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm run build -- --base=/onelink-mock/
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OneLink 挡板服务</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location = /onelink-mock {
|
||||
return 301 /onelink-mock/;
|
||||
}
|
||||
|
||||
location /onelink-mock/assets/ {
|
||||
alias /usr/share/nginx/html/assets/;
|
||||
}
|
||||
|
||||
location /onelink-mock/api/ {
|
||||
rewrite ^/onelink-mock/(.*)$ /$1 break;
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /onelink-mock/v5/ {
|
||||
rewrite ^/onelink-mock/(.*)$ /$1 break;
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /onelink-mock/ec/ {
|
||||
rewrite ^/onelink-mock/(.*)$ /$1 break;
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /onelink-mock/healthz {
|
||||
rewrite ^/onelink-mock/(.*)$ /$1 break;
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /onelink-mock/ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /v5/ {
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /ec/ {
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location /healthz {
|
||||
proxy_pass http://backend:8080;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "onelink-baffle-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"packageManager": "pnpm@11.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"vite": "^7.2.7",
|
||||
"vue": "^3.5.25",
|
||||
"lucide-vue-next": "^0.561.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
Generated
+856
@@ -0,0 +1,856 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.8(vite@7.3.6)(vue@3.5.41)
|
||||
lucide-vue-next:
|
||||
specifier: ^0.561.0
|
||||
version: 0.561.0(vue@3.5.41)
|
||||
vite:
|
||||
specifier: ^7.2.7
|
||||
version: 7.3.6
|
||||
vue:
|
||||
specifier: ^3.5.25
|
||||
version: 3.5.41
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/helper-string-parser@7.29.7':
|
||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7':
|
||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.2':
|
||||
resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.28.2':
|
||||
resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.28.2':
|
||||
resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.28.2':
|
||||
resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.28.2':
|
||||
resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.28.2':
|
||||
resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.28.2':
|
||||
resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.2':
|
||||
resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.2':
|
||||
resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.2':
|
||||
resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.28.2':
|
||||
resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.28.2':
|
||||
resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.28.2':
|
||||
resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.28.2':
|
||||
resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
|
||||
engines: {node: ^22.20 || ^24.12 || >=25}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/pluginutils@1.0.1':
|
||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.0':
|
||||
resolution: {integrity: sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.0':
|
||||
resolution: {integrity: sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.0':
|
||||
resolution: {integrity: sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.0':
|
||||
resolution: {integrity: sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.0':
|
||||
resolution: {integrity: sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.0':
|
||||
resolution: {integrity: sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.0':
|
||||
resolution: {integrity: sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.0':
|
||||
resolution: {integrity: sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.0':
|
||||
resolution: {integrity: sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.0':
|
||||
resolution: {integrity: sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.0':
|
||||
resolution: {integrity: sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.0':
|
||||
resolution: {integrity: sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.0':
|
||||
resolution: {integrity: sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.0':
|
||||
resolution: {integrity: sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.0':
|
||||
resolution: {integrity: sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.0':
|
||||
resolution: {integrity: sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.0':
|
||||
resolution: {integrity: sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.0':
|
||||
resolution: {integrity: sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.0':
|
||||
resolution: {integrity: sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@vitejs/plugin-vue@6.0.8':
|
||||
resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@vue/compiler-core@3.5.41':
|
||||
resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==}
|
||||
|
||||
'@vue/compiler-dom@3.5.41':
|
||||
resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.41':
|
||||
resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==}
|
||||
|
||||
'@vue/compiler-ssr@3.5.41':
|
||||
resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==}
|
||||
|
||||
'@vue/reactivity@3.5.41':
|
||||
resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==}
|
||||
|
||||
'@vue/runtime-core@3.5.41':
|
||||
resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==}
|
||||
|
||||
'@vue/runtime-dom@3.5.41':
|
||||
resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==}
|
||||
|
||||
'@vue/server-renderer@3.5.41':
|
||||
resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==}
|
||||
|
||||
'@vue/shared@3.5.41':
|
||||
resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
entities@7.0.1:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild@0.28.2:
|
||||
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
lucide-vue-next@0.561.0:
|
||||
resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.7:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postcss@8.5.26:
|
||||
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@4.63.0:
|
||||
resolution: {integrity: sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
vite@7.3.6:
|
||||
resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
jiti: '>=1.21.0'
|
||||
less: ^4.0.0
|
||||
lightningcss: ^1.21.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
sugarss: ^5.0.0
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vue@3.5.41:
|
||||
resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.29.7': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7': {}
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.1': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.0':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@vitejs/plugin-vue@6.0.8(vite@7.3.6)(vue@3.5.41)':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
vite: 7.3.6
|
||||
vue: 3.5.41
|
||||
|
||||
'@vue/compiler-core@3.5.41':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@vue/shared': 3.5.41
|
||||
entities: 7.0.1
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-dom@3.5.41':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
'@vue/compiler-sfc@3.5.41':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@vue/compiler-core': 3.5.41
|
||||
'@vue/compiler-dom': 3.5.41
|
||||
'@vue/compiler-ssr': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.21
|
||||
postcss: 8.5.26
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-ssr@3.5.41':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
'@vue/reactivity@3.5.41':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
'@vue/runtime-core@3.5.41':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
'@vue/runtime-dom@3.5.41':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.41
|
||||
'@vue/runtime-core': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
csstype: 3.2.3
|
||||
|
||||
'@vue/server-renderer@3.5.41':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.41
|
||||
'@vue/runtime-dom': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
'@vue/shared@3.5.41': {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
esbuild@0.28.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.28.2
|
||||
'@esbuild/android-arm': 0.28.2
|
||||
'@esbuild/android-arm64': 0.28.2
|
||||
'@esbuild/android-x64': 0.28.2
|
||||
'@esbuild/darwin-arm64': 0.28.2
|
||||
'@esbuild/darwin-x64': 0.28.2
|
||||
'@esbuild/freebsd-arm64': 0.28.2
|
||||
'@esbuild/freebsd-x64': 0.28.2
|
||||
'@esbuild/linux-arm': 0.28.2
|
||||
'@esbuild/linux-arm64': 0.28.2
|
||||
'@esbuild/linux-ia32': 0.28.2
|
||||
'@esbuild/linux-loong64': 0.28.2
|
||||
'@esbuild/linux-mips64el': 0.28.2
|
||||
'@esbuild/linux-ppc64': 0.28.2
|
||||
'@esbuild/linux-riscv64': 0.28.2
|
||||
'@esbuild/linux-s390x': 0.28.2
|
||||
'@esbuild/linux-x64': 0.28.2
|
||||
'@esbuild/netbsd-arm64': 0.28.2
|
||||
'@esbuild/netbsd-x64': 0.28.2
|
||||
'@esbuild/openbsd-arm64': 0.28.2
|
||||
'@esbuild/openbsd-x64': 0.28.2
|
||||
'@esbuild/openharmony-arm64': 0.28.2
|
||||
'@esbuild/sunos-x64': 0.28.2
|
||||
'@esbuild/win32-arm64': 0.28.2
|
||||
'@esbuild/win32-ia32': 0.28.2
|
||||
'@esbuild/win32-x64': 0.28.2
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.7):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.7
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
lucide-vue-next@0.561.0(vue@3.5.41):
|
||||
dependencies:
|
||||
vue: 3.5.41
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
postcss@8.5.26:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@4.63.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
optionalDependencies:
|
||||
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
|
||||
'@rollup/rollup-android-arm-eabi': 4.63.0
|
||||
'@rollup/rollup-android-arm64': 4.63.0
|
||||
'@rollup/rollup-darwin-arm64': 4.63.0
|
||||
'@rollup/rollup-darwin-x64': 4.63.0
|
||||
'@rollup/rollup-freebsd-arm64': 4.63.0
|
||||
'@rollup/rollup-freebsd-x64': 4.63.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.63.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.63.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.63.0
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-loong64-musl': 4.63.0
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.63.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.63.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.63.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.63.0
|
||||
'@rollup/rollup-openbsd-x64': 4.63.0
|
||||
'@rollup/rollup-openharmony-arm64': 4.63.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.63.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.63.0
|
||||
'@rollup/rollup-win32-x64-gnu': 4.63.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.63.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
|
||||
vite@7.3.6:
|
||||
dependencies:
|
||||
esbuild: 0.28.2
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.26
|
||||
rollup: 4.63.0
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
vue@3.5.41:
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.41
|
||||
'@vue/compiler-sfc': 3.5.41
|
||||
'@vue/runtime-dom': 3.5.41
|
||||
'@vue/server-renderer': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -0,0 +1,548 @@
|
||||
import { createApp, computed, onMounted, reactive, ref } from 'vue/dist/vue.esm-bundler.js'
|
||||
import { ArrowLeft, BadgePlus, ClipboardList, Copy, CreditCard, Database, Download, Eye, FolderPlus, KeyRound, Plus, RefreshCw, Save, Search, Server, Trash2, X } from 'lucide-vue-next'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import './style.css'
|
||||
|
||||
const appBase = normalizeBase(import.meta.env.BASE_URL)
|
||||
|
||||
const statusText = {
|
||||
'1': '待激活',
|
||||
'2': '已激活',
|
||||
'4': '停机',
|
||||
'6': '可测试',
|
||||
'7': '库存',
|
||||
'8': '预销户'
|
||||
}
|
||||
|
||||
const detailLabels = {
|
||||
msisdn: 'MSISDN', iccid: 'ICCID', imsi: 'IMSI', imei: 'IMEI', remark: '备注',
|
||||
cardStatus: '卡状态', lastChangeDate: '最后变更时间', stopReason: '停机原因码',
|
||||
frozenStatus: '管理冻结', frozenReason: '冻结原因', frozenOperTime: '冻结操作时间',
|
||||
openDate: '开卡时间', activeDate: '首次激活时间', onOffStatus: '终端开关机',
|
||||
sessionStatus: '会话状态', sessionCreateDate: '会话建立时间', ip: 'IPv4', ipv6Prefix: 'IPv6 前缀',
|
||||
ipv6: 'IPv6 接口 ID', rat: '接入制式', cardBindStatus: '机卡绑定', regionLimitStatus: '区域限制',
|
||||
areaRestrictType: '区域限制方式', restrictedArea: '限制区域', monthUsageKB: '当月流量 KB',
|
||||
dailyBaseKB: '日基础流量 KB', monthlyBaseKB: '月基础流量 KB', lastUsageUpdateTime: '流量更新时间'
|
||||
}
|
||||
|
||||
function detailLabel(key) { return detailLabels[key] || key }
|
||||
function cardStatusLabel(value) { return statusText[value] || value || '-' }
|
||||
function boolLabel(value) { return value === '1' || value === 1 || value === true ? '是' : '否' }
|
||||
function apnDiagnosisStatusLabel(value) { return ({ '1': '未激活', '2': '激活', '4': '暂停' })[value] || value || '-' }
|
||||
function apnServiceUsageLabel(value) { return value === '1' || !value ? '正常速率' : `限速档位 ${value}` }
|
||||
|
||||
const endpointGroups = [
|
||||
{
|
||||
key: 'mobile-core', label: '移动基础接口', items: [
|
||||
['ec/query/sim-basic-info', '单卡基础信息'], ['ec/query/sim-session', '单卡会话信息'], ['ec/query/sim-imei', '查询 IMEI'],
|
||||
['ec/query/sim-data-usage-inpool', '查询流量池用量'], ['ec/query/sim-data-usage', '查询流量用量'],
|
||||
['ec/query/sim-data-usage-monthly/batch', '批量查询月流量'], ['ec/query/sim-data-usage-daily/batch', '批量查询日流量'],
|
||||
['ec/query/apn-info', '查询 APN'], ['ec/change/sim-status', '变更 SIM 状态'], ['ec/operate/sim-apn-function', '操作 APN']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'mobile-diagnosis', label: '移动智能诊断', items: [
|
||||
['ec/query/sim-status', 'SIM 卡状态'], ['ec/query/sim-stop-reason', '停机原因'], ['ec/query/sim-frozen-state-info', '管理冻结状态'],
|
||||
['ec/query/sim-apn-session-info', 'APN 在线会话'], ['ec/query/sim-activated-features-info', '通信功能状态'],
|
||||
['ec/query/sim-operating-status', '开关机状态'], ['ec/query/sim-data-diagnosis', '达量限速档位'],
|
||||
['ec/query/region-limit-status', '区域限制状态'], ['ec/query/card-bind-status', '机卡绑定状态'], ['ec/query/on-off-status', '设备开关机状态']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'chan', label: 'Chan 统一接口', items: [
|
||||
['sim/query/sim-basic-info', '单卡基础信息'], ['sim/query/sim-session', '单卡会话信息'], ['sim/query/sim-status', 'SIM 卡状态'],
|
||||
['sim/query/sim-status-batch', '批量 SIM 卡状态'], ['sim/query/sim-card-bind-status', '机卡绑定状态'], ['sim/query/sim-device-status', '设备状态'],
|
||||
['sim/query/sim-region-limit-status', '区域限制状态'], ['sim/query/sim-imei', '查询 IMEI'], ['sim/query/sim-datausage', '查询流量'],
|
||||
['sim/query/sim-datausage-monthly', '查询月流量'], ['sim/query/sim-datausage-monthly-batch', '批量查询月流量'],
|
||||
['sim/query/sim-datausage-daily', '查询日流量'], ['sim/query/sim-datausage-daily-batch', '批量查询日流量'],
|
||||
['sim/query/sim-apn-info', '查询 APN'], ['sim/change/sim-status', '变更 SIM 状态'], ['sim/change/sim-apn', '变更 APN']
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const endpointCatalog = endpointGroups.flatMap(group => group.items.map(([path, label]) => ({ path, label, group: group.key })))
|
||||
|
||||
const App = {
|
||||
components: { ArrowLeft, BadgePlus, ClipboardList, Copy, CreditCard, Database, Download, Eye, FolderPlus, KeyRound, Plus, RefreshCw, Save, Search, Server, Trash2, X },
|
||||
setup() {
|
||||
const state = reactive({ appids: [], groups: [], cards: [], orders: [], settings: {} })
|
||||
const message = ref('')
|
||||
const appForm = reactive({ appid: '', password: '', name: '' })
|
||||
const cardForm = reactive(emptyCard())
|
||||
const selectedCategory = ref('mobile-core')
|
||||
const selectedEndpoint = ref('ec/query/sim-basic-info')
|
||||
const generateCount = ref(100)
|
||||
const selectedGroupId = ref(0)
|
||||
const groupForm = reactive({ name: '' })
|
||||
const isGroupEditorOpen = ref(false)
|
||||
const groupSaving = ref(false)
|
||||
const selectedCards = reactive(new Set())
|
||||
const cardKeyword = ref('')
|
||||
const cardGroupFilter = ref('')
|
||||
const cardExportStatusFilter = ref('')
|
||||
const cardPage = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const orderPage = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const cardJumpPage = ref(1)
|
||||
const orderJumpPage = ref(1)
|
||||
const cardTotalPages = computed(() => Math.max(1, Math.ceil(cardPage.total / cardPage.pageSize)))
|
||||
const orderTotalPages = computed(() => Math.max(1, Math.ceil(orderPage.total / orderPage.pageSize)))
|
||||
const selectedCardDetail = ref(null)
|
||||
const selectedOrderDetail = ref(null)
|
||||
const isCardEditorOpen = ref(false)
|
||||
const cardEditorMode = ref('create')
|
||||
const cardSaving = ref(false)
|
||||
const currentView = ref(viewFromPath())
|
||||
const operationLogs = ref([])
|
||||
const operationLogPage = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const operationLogKeyword = ref('')
|
||||
const operationLogCategory = ref('')
|
||||
const operationLogResult = ref('')
|
||||
const selectedOperationLog = ref(null)
|
||||
const operationLogTotalPages = computed(() => Math.max(1, Math.ceil(operationLogPage.total / operationLogPage.pageSize)))
|
||||
const visibleEndpoints = computed(() => endpointCatalog.filter(item => item.group === selectedCategory.value))
|
||||
const currentEndpoint = computed(() => endpointCatalog.find(item => item.path === selectedEndpoint.value) || endpointCatalog[0])
|
||||
const isChanEndpoint = computed(() => selectedEndpoint.value.startsWith('sim/'))
|
||||
const endpointMethod = computed(() => isChanEndpoint.value ? 'POST' : 'GET')
|
||||
|
||||
const baseUrl = computed(() => `${location.origin}${appBase}/v5/`)
|
||||
const sampleCard = computed(() => state.cards[0] || { msisdn: '1440292201313' })
|
||||
const selectedCount = computed(() => selectedCards.size)
|
||||
const allCardsSelected = computed(() => state.cards.length > 0 && state.cards.every(card => selectedCards.has(card.msisdn)))
|
||||
const firstTokenUrl = computed(() => {
|
||||
const app = state.appids[0] || { appid: 'demo_appid', password: 'demo_password' }
|
||||
return `${baseUrl.value}ec/get/token?appid=${app.appid}&password=${app.password}&transid=${transid(app.appid)}`
|
||||
})
|
||||
const endpointUrl = computed(() => {
|
||||
if (isChanEndpoint.value) {
|
||||
return `${location.origin}${appBase}/${selectedEndpoint.value}`
|
||||
}
|
||||
const token = state.appids[0]?.lastToken || '<token>'
|
||||
const params = new URLSearchParams({ transid: transid(state.appids[0]?.appid || 'appid'), token })
|
||||
if (selectedEndpoint.value.includes('/batch')) {
|
||||
params.set('msisdns', state.cards.map(c => c.msisdn).slice(0, 3).join('_') || sampleCard.value.msisdn)
|
||||
params.set('queryDate', selectedEndpoint.value.includes('monthly') ? monthText() : yesterday())
|
||||
} else if (selectedEndpoint.value.includes('card-bind-status')) {
|
||||
params.set('msisdn', sampleCard.value.msisdn)
|
||||
params.set('testType', '0')
|
||||
} else if (selectedEndpoint.value.includes('change/sim-status')) {
|
||||
params.set('msisdn', sampleCard.value.msisdn)
|
||||
params.set('operType', '0')
|
||||
} else if (selectedEndpoint.value.includes('operate/sim-apn-function')) {
|
||||
params.set('msisdn', sampleCard.value.msisdn)
|
||||
params.set('apnName', 'CMIOT')
|
||||
params.set('operType', '1')
|
||||
} else {
|
||||
params.set('msisdn', sampleCard.value.msisdn)
|
||||
}
|
||||
return `${baseUrl.value}${selectedEndpoint.value}?${params.toString()}`
|
||||
})
|
||||
const endpointBody = computed(() => {
|
||||
if (!isChanEndpoint.value) return ''
|
||||
const cards = state.cards.slice(0, 3)
|
||||
const simKeys = (cards.length ? cards : [sampleCard.value]).map(card => ({
|
||||
chanKey: 'channel_yd_mock',
|
||||
iccid: card.iccid || '',
|
||||
imsi: card.imsi || '',
|
||||
msisdn: card.msisdn || ''
|
||||
}))
|
||||
const reqData = {}
|
||||
if (selectedEndpoint.value.includes('monthly')) reqData.queryDate = monthText()
|
||||
if (selectedEndpoint.value.includes('daily')) reqData.queryDate = yesterday()
|
||||
if (selectedEndpoint.value === 'sim/change/sim-status') reqData.operType = '0'
|
||||
if (selectedEndpoint.value === 'sim/change/sim-apn') Object.assign(reqData, { apnName: 'CMIOT', operType: '0' })
|
||||
return JSON.stringify({ simKeys, reqData }, null, 2)
|
||||
})
|
||||
const endpointHeaders = computed(() => '')
|
||||
|
||||
function selectCategory() {
|
||||
const first = visibleEndpoints.value[0]
|
||||
if (first) selectedEndpoint.value = first.path
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [snapshot, groups, cards, orders, logs] = await Promise.all([request('/api/state'), request('/api/card-groups'), loadCards(), loadOrders(), loadOperationLogs(1, 1)])
|
||||
Object.assign(state, snapshot, { groups, cards: cards.items, orders: orders.items })
|
||||
Object.assign(cardPage, { page: cards.page, pageSize: cards.pageSize, total: cards.total })
|
||||
Object.assign(orderPage, { page: orders.page, pageSize: orders.pageSize, total: orders.total })
|
||||
cardJumpPage.value = cards.page
|
||||
orderJumpPage.value = orders.page
|
||||
operationLogPage.total = logs.total
|
||||
for (const msisdn of Array.from(selectedCards)) {
|
||||
if (!state.cards.some(card => card.msisdn === msisdn)) selectedCards.delete(msisdn)
|
||||
}
|
||||
}
|
||||
function loadOperationLogs(page = operationLogPage.page, pageSize = operationLogPage.pageSize) {
|
||||
const params = new URLSearchParams({ page, pageSize })
|
||||
if (operationLogKeyword.value.trim()) params.set('keyword', operationLogKeyword.value.trim())
|
||||
if (operationLogCategory.value) params.set('category', operationLogCategory.value)
|
||||
if (operationLogResult.value) params.set('result', operationLogResult.value)
|
||||
return request(`/api/operation-logs?${params.toString()}`)
|
||||
}
|
||||
async function refreshOperationLogs() {
|
||||
const result = await loadOperationLogs()
|
||||
operationLogs.value = result.items
|
||||
Object.assign(operationLogPage, { page: result.page, pageSize: result.pageSize, total: result.total })
|
||||
}
|
||||
async function searchOperationLogs() {
|
||||
operationLogPage.page = 1
|
||||
await refreshOperationLogs()
|
||||
}
|
||||
async function clearOperationLogSearch() {
|
||||
operationLogKeyword.value = ''
|
||||
operationLogCategory.value = ''
|
||||
operationLogResult.value = ''
|
||||
await searchOperationLogs()
|
||||
}
|
||||
async function changeOperationLogPage(page) {
|
||||
operationLogPage.page = page
|
||||
await refreshOperationLogs()
|
||||
}
|
||||
async function changeOperationLogPageSize(pageSize) {
|
||||
operationLogPage.pageSize = Number(pageSize)
|
||||
operationLogPage.page = 1
|
||||
await refreshOperationLogs()
|
||||
}
|
||||
async function navigate(view) {
|
||||
const path = view === 'operation-logs' ? withBase('/operation-logs') : (appBase || '/')
|
||||
history.pushState({}, '', path)
|
||||
currentView.value = view
|
||||
if (view === 'operation-logs') await refreshOperationLogs()
|
||||
}
|
||||
function handlePopState() {
|
||||
currentView.value = viewFromPath()
|
||||
if (currentView.value === 'operation-logs') refreshOperationLogs()
|
||||
}
|
||||
function scrollToOrders() {
|
||||
document.getElementById('recent-orders')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
function loadCards() {
|
||||
const params = new URLSearchParams({ page: cardPage.page, pageSize: cardPage.pageSize })
|
||||
appendCardFilters(params)
|
||||
return request(`/api/cards?${params.toString()}`)
|
||||
}
|
||||
function appendCardFilters(params) {
|
||||
if (cardKeyword.value.trim()) params.set('keyword', cardKeyword.value.trim())
|
||||
if (cardGroupFilter.value !== '') params.set('groupId', cardGroupFilter.value)
|
||||
if (cardExportStatusFilter.value) params.set('exportStatus', cardExportStatusFilter.value)
|
||||
}
|
||||
function loadOrders() { return request(`/api/orders?page=${orderPage.page}&pageSize=${orderPage.pageSize}`) }
|
||||
async function searchCards() { cardPage.page = 1; await searchCardsPage() }
|
||||
async function clearCardSearch() { cardKeyword.value = ''; cardGroupFilter.value = ''; cardExportStatusFilter.value = ''; await searchCards() }
|
||||
async function filterCardsByGroup() { cardPage.page = 1; await searchCardsPage() }
|
||||
async function changeCardPage(nextPage) { cardPage.page = nextPage; await searchCardsPage() }
|
||||
async function changeCardPageSize(pageSize) { cardPage.pageSize = Number(pageSize); cardPage.page = 1; await searchCardsPage() }
|
||||
async function searchCardsPage() {
|
||||
const cards = await loadCards()
|
||||
state.cards = cards.items
|
||||
Object.assign(cardPage, { page: cards.page, pageSize: cards.pageSize, total: cards.total })
|
||||
cardJumpPage.value = cards.page
|
||||
}
|
||||
async function changeOrderPage(nextPage) { orderPage.page = nextPage; await refreshOrders() }
|
||||
async function changeOrderPageSize(pageSize) { orderPage.pageSize = Number(pageSize); orderPage.page = 1; await refreshOrders() }
|
||||
async function refreshOrders() {
|
||||
const orders = await loadOrders()
|
||||
state.orders = orders.items
|
||||
Object.assign(orderPage, { page: orders.page, pageSize: orders.pageSize, total: orders.total })
|
||||
orderJumpPage.value = orders.page
|
||||
}
|
||||
async function goToCardPage() {
|
||||
let p = Number(cardJumpPage.value)
|
||||
if (!p || p < 1) p = 1
|
||||
if (p > cardTotalPages.value) p = cardTotalPages.value
|
||||
cardJumpPage.value = p
|
||||
if (p !== cardPage.page) {
|
||||
cardPage.page = p
|
||||
await searchCardsPage()
|
||||
}
|
||||
}
|
||||
async function goToOrderPage() {
|
||||
let p = Number(orderJumpPage.value)
|
||||
if (!p || p < 1) p = 1
|
||||
if (p > orderTotalPages.value) p = orderTotalPages.value
|
||||
orderJumpPage.value = p
|
||||
if (p !== orderPage.page) {
|
||||
orderPage.page = p
|
||||
await refreshOrders()
|
||||
}
|
||||
}
|
||||
function goCardFirst() { changeCardPage(1) }
|
||||
function goCardLast() { changeCardPage(cardTotalPages.value) }
|
||||
function goOrderFirst() { changeOrderPage(1) }
|
||||
function goOrderLast() { changeOrderPage(orderTotalPages.value) }
|
||||
async function generateCards() {
|
||||
const result = await request('/api/generate', { method: 'POST', body: { count: generateCount.value, groupId: selectedGroupId.value || null } })
|
||||
await load()
|
||||
const group = state.groups.find(item => item.id === selectedGroupId.value)
|
||||
toast(`已生成 ${result.generated} 张卡${group ? `,已放入“${group.name}”` : '(未分组)'},共 ${result.total} 张`)
|
||||
}
|
||||
function openGroupEditor() { groupForm.name = ''; isGroupEditorOpen.value = true }
|
||||
async function createGroup() {
|
||||
groupSaving.value = true
|
||||
try {
|
||||
const group = await request('/api/card-groups', { method: 'POST', body: groupForm })
|
||||
state.groups.push(group)
|
||||
selectedGroupId.value = group.id
|
||||
isGroupEditorOpen.value = false
|
||||
} finally { groupSaving.value = false }
|
||||
}
|
||||
async function exportCards() {
|
||||
if (!selectedCards.size) {
|
||||
toast('请选择要导出的卡')
|
||||
return
|
||||
}
|
||||
const params = new URLSearchParams({ msisdns: Array.from(selectedCards).join(',') })
|
||||
await downloadCardExport(params, selectedCards.size)
|
||||
}
|
||||
async function exportFilteredCards() {
|
||||
if (!cardPage.total) {
|
||||
toast('当前筛选条件下没有可导出的卡')
|
||||
return
|
||||
}
|
||||
const exportCount = cardPage.total
|
||||
const params = new URLSearchParams({ selection: 'filtered' })
|
||||
appendCardFilters(params)
|
||||
await downloadCardExport(params, exportCount)
|
||||
}
|
||||
async function downloadCardExport(params, exportCount) {
|
||||
const response = await fetch(withBase(`/api/cards/export?${params.toString()}`))
|
||||
if (!response.ok) throw new Error(await response.text())
|
||||
const downloadURL = URL.createObjectURL(await response.blob())
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadURL
|
||||
link.download = 'sim_card_export.xlsx'
|
||||
link.click()
|
||||
URL.revokeObjectURL(downloadURL)
|
||||
await searchCardsPage()
|
||||
toast(`已导出 ${exportCount} 张卡`)
|
||||
}
|
||||
function toggleCard(msisdn) {
|
||||
if (selectedCards.has(msisdn)) selectedCards.delete(msisdn)
|
||||
else selectedCards.add(msisdn)
|
||||
}
|
||||
function toggleAllCards() {
|
||||
if (allCardsSelected.value) {
|
||||
selectedCards.clear()
|
||||
} else {
|
||||
state.cards.forEach(card => selectedCards.add(card.msisdn))
|
||||
}
|
||||
}
|
||||
async function saveApp() {
|
||||
await request('/api/appids', { method: 'POST', body: appForm })
|
||||
Object.assign(appForm, { appid: '', password: '', name: '' })
|
||||
await load()
|
||||
toast('AppID 已保存')
|
||||
}
|
||||
async function deleteApp(appid) {
|
||||
await request(`/api/appids?appid=${encodeURIComponent(appid)}`, { method: 'DELETE' })
|
||||
await load()
|
||||
}
|
||||
function openCardEditor(card = null) {
|
||||
cardEditorMode.value = card ? 'edit' : 'create'
|
||||
Object.assign(cardForm, JSON.parse(JSON.stringify(card || emptyCard())))
|
||||
isCardEditorOpen.value = true
|
||||
}
|
||||
async function saveCard() {
|
||||
cardSaving.value = true
|
||||
try {
|
||||
await request('/api/cards', { method: 'POST', body: cardForm })
|
||||
isCardEditorOpen.value = false
|
||||
Object.assign(cardForm, emptyCard())
|
||||
await load()
|
||||
toast(cardEditorMode.value === 'edit' ? '卡数据已更新' : '卡数据已保存')
|
||||
} finally { cardSaving.value = false }
|
||||
}
|
||||
async function deleteCard(msisdn) {
|
||||
await request(`/api/cards?msisdn=${encodeURIComponent(msisdn)}`, { method: 'DELETE' })
|
||||
await load()
|
||||
}
|
||||
async function copyText(value) {
|
||||
if (value === null || value === undefined) {
|
||||
toast('复制内容为空')
|
||||
return
|
||||
}
|
||||
const text = String(value)
|
||||
if (!text) {
|
||||
toast('复制内容为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
toast('已复制到剪贴板')
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', 'readonly')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
textarea.style.left = '-9999px'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
textarea.setSelectionRange(0, textarea.value.length)
|
||||
const ok = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
toast(ok ? '已复制到剪贴板' : '复制失败,请手动复制')
|
||||
} catch (_) {
|
||||
toast('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
async function request(path, options = {}) {
|
||||
const init = { ...options, headers: { 'Content-Type': 'application/json' } }
|
||||
if (options.body) init.body = JSON.stringify(options.body)
|
||||
const res = await fetch(withBase(path), init)
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
return res.json()
|
||||
}
|
||||
function toast(text) {
|
||||
message.value = text
|
||||
setTimeout(() => { if (message.value === text) message.value = '' }, 2200)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
if (currentView.value === 'operation-logs') await refreshOperationLogs()
|
||||
else await load()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('popstate', handlePopState))
|
||||
return { state, message, appForm, cardForm, selectedCategory, selectedEndpoint, endpointGroups, visibleEndpoints, currentEndpoint, firstTokenUrl, endpointUrl, endpointBody, endpointHeaders, endpointMethod, isChanEndpoint, statusText, detailLabel, cardStatusLabel, boolLabel, apnDiagnosisStatusLabel, apnServiceUsageLabel, generateCount, selectedGroupId, groupForm, isGroupEditorOpen, groupSaving, selectedCards, selectedCount, allCardsSelected, cardKeyword, cardGroupFilter, cardExportStatusFilter, cardPage, orderPage, cardJumpPage, orderJumpPage, cardTotalPages, orderTotalPages, selectedCardDetail, selectedOrderDetail, isCardEditorOpen, cardEditorMode, cardSaving, currentView, operationLogs, operationLogPage, operationLogKeyword, operationLogCategory, operationLogResult, operationLogTotalPages, selectedOperationLog, load, selectCategory, searchCards, clearCardSearch, filterCardsByGroup, changeCardPage, changeCardPageSize, changeOrderPage, changeOrderPageSize, goToCardPage, goToOrderPage, goCardFirst, goCardLast, goOrderFirst, goOrderLast, generateCards, openGroupEditor, createGroup, exportCards, exportFilteredCards, toggleCard, toggleAllCards, saveApp, deleteApp, openCardEditor, saveCard, deleteCard, copyText, navigate, scrollToOrders, searchOperationLogs, clearOperationLogSearch, changeOperationLogPage, changeOperationLogPageSize }
|
||||
},
|
||||
template: `
|
||||
<main class="shell">
|
||||
<div v-if="currentView === 'dashboard'">
|
||||
<header class="topbar">
|
||||
<div><p class="eyebrow">OneLink V25.11.1 CT Mock</p><h1>挡板服务管理台</h1></div>
|
||||
<div class="top-actions">
|
||||
<input v-model.number="generateCount" class="count-input" type="number" min="1" />
|
||||
<select v-model.number="selectedGroupId" class="group-select"><option :value="0">未分组</option><option v-for="group in state.groups" :key="group.id" :value="group.id">{{ group.name }}</option></select>
|
||||
<button class="ghost icon-only" @click="openGroupEditor" title="新建分组"><FolderPlus :size="16" /></button>
|
||||
<button @click="generateCards"><RefreshCw :size="16" />一键生成</button>
|
||||
<button class="ghost" @click="exportCards"><Download :size="16" />导出选中 {{ selectedCount }}</button>
|
||||
<button class="ghost" :disabled="!cardPage.total" @click="exportFilteredCards"><Download :size="16" />导出筛选结果 {{ cardPage.total }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="metrics">
|
||||
<div><KeyRound :size="19" /><strong>{{ state.appids.length }}</strong><span>AppID</span></div>
|
||||
<div><CreditCard :size="19" /><strong>{{ cardPage.total }}</strong><span>SIM 卡</span></div>
|
||||
<div><Database :size="19" /><strong>18432</strong><span>固定月 KB</span></div>
|
||||
<div class="metric-link" role="link" tabindex="0" @click="scrollToOrders" @keyup.enter="scrollToOrders"><Database :size="19" /><strong>{{ orderPage.total }}</strong><span>办理订单</span></div>
|
||||
<div class="metric-link" role="link" tabindex="0" @click="navigate('operation-logs')" @keyup.enter="navigate('operation-logs')"><ClipboardList :size="19" /><strong>{{ operationLogPage.total }}</strong><span>操作日志</span></div>
|
||||
</section>
|
||||
|
||||
<section class="band">
|
||||
<div class="section-head"><div><h2>AppID 配置</h2><p>支持多个 appid/password,Token 有效期 3600 秒。</p></div><KeyRound :size="22" /></div>
|
||||
<div class="form-grid app-grid">
|
||||
<input v-model="appForm.appid" placeholder="appid" />
|
||||
<input v-model="appForm.password" placeholder="password" />
|
||||
<input v-model="appForm.name" placeholder="名称" />
|
||||
<button @click="saveApp"><Plus :size="16" />保存</button>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="tr th"><span>名称</span><span>appid</span><span>最近 token</span><span></span></div>
|
||||
<div class="tr" v-for="app in state.appids" :key="app.appid">
|
||||
<span>{{ app.name }}</span><span>{{ app.appid }}</span><span class="mono">{{ app.lastToken || '-' }}</span>
|
||||
<button class="ghost" @click="deleteApp(app.appid)" title="删除"><Trash2 :size="15" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band">
|
||||
<div class="section-head"><div><h2>卡与流量数据</h2><p>配置码号、状态、APN、在线状态和固定月流量。</p></div><div class="section-head-actions"><button @click="openCardEditor()"><Plus :size="16" />新增卡</button><CreditCard :size="22" /></div></div>
|
||||
<div class="search-row"><input v-model="cardKeyword" @keyup.enter="searchCards" placeholder="输入 MSISDN / ICCID / IMSI" /><select v-model="cardGroupFilter" @change="filterCardsByGroup"><option value="">全部分组</option><option value="0">未分组</option><option v-for="group in state.groups" :key="group.id" :value="String(group.id)">{{ group.name }}</option></select><select v-model="cardExportStatusFilter" @change="filterCardsByGroup"><option value="">全部导出状态</option><option value="exported">已导出</option><option value="not_exported">未导出</option></select><button @click="searchCards"><Search :size="16" />查询</button><button class="ghost icon-only" @click="clearCardSearch" title="清空查询"><X :size="16" /></button></div>
|
||||
<div class="table cards">
|
||||
<div class="tr th"><span><input class="select-box" type="checkbox" :checked="allCardsSelected" @change="toggleAllCards" /></span><span>MSISDN</span><span>ICCID</span><span>IMSI</span><span>分组</span><span>状态</span><span>导出状态</span><span>在线</span><span>当月 KB</span><span></span></div>
|
||||
<div class="tr" v-for="card in state.cards" :key="card.msisdn">
|
||||
<span><input class="select-box" type="checkbox" :checked="selectedCards.has(card.msisdn)" @change="toggleCard(card.msisdn)" /></span>
|
||||
<span class="mono">{{ card.msisdn }}</span><span class="mono">{{ card.iccid }}</span><span class="mono">{{ card.imsi }}</span><span>{{ card.groupName || '未分组' }}</span><span>{{ statusText[card.cardStatus] || card.cardStatus }}</span><span><em class="export-status" :class="card.exported ? 'exported' : 'not-exported'">{{ card.exported ? '已导出' : '未导出' }}</em></span><span>{{ card.sessionStatus === '01' ? '在线' : '离线' }}</span><span>{{ Math.round(card.monthUsageKB) }}</span>
|
||||
<span class="row-actions"><button class="ghost icon-only" @click="selectedCardDetail = card" title="详情"><Eye :size="15" /></button><button class="ghost" @click="openCardEditor(card)">编辑</button><button class="ghost icon-only" @click="deleteCard(card.msisdn)" title="删除"><Trash2 :size="15" /></button></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pager"><span>共 {{ cardPage.total }} 张卡</span><label class="page-size"><span>每页</span><select :value="cardPage.pageSize" @change="changeCardPageSize($event.target.value)"><option v-for="size in [10,20,50,100]" :key="size" :value="size">{{ size }} 条</option></select></label><button class="ghost" :disabled="cardPage.page <= 1" @click="goCardFirst">首页</button><button class="ghost" :disabled="cardPage.page <= 1" @click="changeCardPage(cardPage.page - 1)">上一页</button><span class="page-jump">第 <input class="page-input" type="number" min="1" :max="cardTotalPages" v-model.number="cardJumpPage" @keyup.enter="goToCardPage" /> 页 / 共 {{ cardTotalPages }} 页</span><button class="ghost" :disabled="cardPage.page >= cardTotalPages" @click="changeCardPage(cardPage.page + 1)">下一页</button><button class="ghost" :disabled="cardPage.page >= cardTotalPages" @click="goCardLast">尾页</button><button class="ghost" @click="goToCardPage">跳转</button></div>
|
||||
</section>
|
||||
|
||||
<section class="split single">
|
||||
<div class="band">
|
||||
<div class="section-head"><div><h2>接口调试</h2><p>支持移动 OneLink 与 Chan 统一接口。</p></div><Server :size="22" /></div>
|
||||
<div class="endpoint-categories" role="tablist" aria-label="接口分类">
|
||||
<button v-for="group in endpointGroups" :key="group.key" class="endpoint-category" :class="{ active: selectedCategory === group.key }" @click="selectedCategory = group.key; selectCategory()">{{ group.label }}<span>{{ group.items.length }}</span></button>
|
||||
</div>
|
||||
<label class="field" v-if="selectedCategory.startsWith('mobile-')"><span>Token 获取</span><div class="copy-field"><input class="mono" :value="firstTokenUrl" readonly /><button class="ghost icon-only" @click="copyText(firstTokenUrl)" title="复制 Token 地址"><Copy :size="16" /></button></div></label>
|
||||
<label class="field"><span>业务接口({{ endpointMethod }})</span><select class="endpoint-select" v-model="selectedEndpoint"><option v-for="item in visibleEndpoints" :key="item.path" :value="item.path">{{ item.label }} · {{ item.path }}</option></select></label>
|
||||
<div class="copy-field"><input class="mono" :value="endpointUrl" readonly /><button class="ghost icon-only" @click="copyText(endpointUrl)" title="复制业务接口地址"><Copy :size="16" /></button></div>
|
||||
<label class="field endpoint-detail" v-if="endpointHeaders"><span>请求 Header</span><div class="copy-field"><textarea class="mono endpoint-body compact" :value="endpointHeaders" readonly></textarea><button class="ghost icon-only" @click="copyText(endpointHeaders)" title="复制请求 Header"><Copy :size="16" /></button></div></label>
|
||||
<label class="field endpoint-detail" v-if="endpointBody"><span>JSON 请求体</span><div class="copy-field"><textarea class="mono endpoint-body" :value="endpointBody" readonly></textarea><button class="ghost icon-only" @click="copyText(endpointBody)" title="复制 JSON 请求体"><Copy :size="16" /></button></div></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band" id="recent-orders">
|
||||
<div class="section-head"><div><h2>最近订单</h2><p>状态变更与 APN 开停会生成 orderNum。</p></div><BadgePlus :size="22" /></div>
|
||||
<div class="empty" v-if="!state.orders.length">暂无订单</div>
|
||||
<div class="table orders" v-else>
|
||||
<div class="tr th"><span>订单号</span><span>类型</span><span>操作</span><span>MSISDN</span><span>状态</span><span>创建时间</span><span></span></div>
|
||||
<div class="tr order-row" v-for="order in state.orders" :key="order.orderNum">
|
||||
<span class="mono">{{ order.orderNum }}</span><span>{{ order.type }}</span><span>{{ order.operType || '-' }}</span><span class="mono">{{ order.msisdn }}</span><span>{{ order.status }}</span><span>{{ order.createdAt }}</span><button class="ghost icon-only" @click="selectedOrderDetail = order" title="详情"><Eye :size="15" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pager" v-if="state.orders.length"><span>共 {{ orderPage.total }} 条订单</span><label class="page-size"><span>每页</span><select :value="orderPage.pageSize" @change="changeOrderPageSize($event.target.value)"><option v-for="size in [10,20,50,100]" :key="size" :value="size">{{ size }} 条</option></select></label><button class="ghost" :disabled="orderPage.page <= 1" @click="goOrderFirst">首页</button><button class="ghost" :disabled="orderPage.page <= 1" @click="changeOrderPage(orderPage.page - 1)">上一页</button><span class="page-jump">第 <input class="page-input" type="number" min="1" :max="orderTotalPages" v-model.number="orderJumpPage" @keyup.enter="goToOrderPage" /> 页 / 共 {{ orderTotalPages }} 页</span><button class="ghost" :disabled="orderPage.page >= orderTotalPages" @click="changeOrderPage(orderPage.page + 1)">下一页</button><button class="ghost" :disabled="orderPage.page >= orderTotalPages" @click="goOrderLast">尾页</button><button class="ghost" @click="goToOrderPage">跳转</button></div>
|
||||
</section>
|
||||
<div class="modal-backdrop" v-if="selectedCardDetail" @click.self="selectedCardDetail = null"><section class="detail-modal card-detail-modal"><div class="detail-head"><div><h2>卡信息详情</h2><p class="mono">{{ selectedCardDetail.msisdn }}</p></div><button class="ghost icon-only" @click="selectedCardDetail = null" title="关闭"><X :size="18" /></button></div><div class="detail-section"><h3>基础信息</h3><div class="detail-grid"><div class="detail-item" v-for="key in ['msisdn','iccid','imsi','imei','remark','openDate','activeDate']" :key="key"><span>{{ detailLabel(key) }}</span><strong :class="['mono', { 'wrap-value': key === 'remark' }]">{{ selectedCardDetail[key] || '-' }}</strong></div></div></div><div class="detail-section"><h3>状态与连接</h3><div class="detail-grid"><div class="detail-item"><span>卡状态</span><strong>{{ cardStatusLabel(selectedCardDetail.cardStatus) }}</strong></div><div class="detail-item"><span>终端开关机</span><strong>{{ boolLabel(selectedCardDetail.onOffStatus) }}</strong></div><div class="detail-item"><span>会话状态</span><strong>{{ selectedCardDetail.sessionStatus === '01' ? '在线' : '离线' }}</strong></div><div class="detail-item"><span>机卡绑定</span><strong>{{ selectedCardDetail.cardBindStatus === '1' ? '未分离' : '已分离' }}</strong></div><div class="detail-item"><span>区域限制</span><strong>{{ selectedCardDetail.regionLimitStatus === '1' ? '已触发' : '未触发' }}</strong></div><div class="detail-item"><span>接入制式</span><strong>{{ selectedCardDetail.rat || '-' }}</strong></div><div class="detail-item"><span>IPv4</span><strong class="mono">{{ selectedCardDetail.ip || '-' }}</strong></div><div class="detail-item"><span>会话建立时间</span><strong class="mono">{{ selectedCardDetail.sessionCreateDate || selectedCardDetail.lastChangeDate || '-' }}</strong></div></div></div><div class="detail-section"><h3>停机与冻结诊断</h3><div class="detail-grid"><div class="detail-item"><span>停机原因码</span><strong class="mono">{{ selectedCardDetail.stopReason || '000000000000' }}</strong></div><div class="detail-item"><span>管理冻结</span><strong>{{ selectedCardDetail.frozenStatus === '1' ? '是' : '否' }}</strong></div><div class="detail-item"><span>冻结原因</span><strong>{{ selectedCardDetail.frozenReason || '-' }}</strong></div><div class="detail-item"><span>冻结操作时间</span><strong class="mono">{{ selectedCardDetail.frozenOperTime || '-' }}</strong></div></div></div><div class="detail-section"><h3>APN 与 PCC 智能诊断</h3><div class="diagnosis-list"><div class="diagnosis-row" v-for="apn in (selectedCardDetail.apns || [])" :key="apn.name"><div class="diagnosis-row-head"><strong>{{ apn.name }}</strong><span>{{ apnDiagnosisStatusLabel(apn.diagnosisStatus || (apn.status === '1' ? '2' : apn.status)) }}</span><span>{{ apnServiceUsageLabel(apn.serviceUsageState) }}</span></div><div class="diagnosis-meta">变更时间 <span class="mono">{{ apn.modifyTime || '-' }}</span><template v-if="apn.overUseDetails"> · 超停原因 <span>{{ apn.overUseDetails }}</span></template></div><div class="pcc-list"><div class="pcc-item" v-for="pcc in (apn.pccs || [])" :key="pcc.serviceCode"><span class="mono">{{ pcc.serviceCode }}</span><span>{{ pcc.name }}</span><span>策略 {{ pcc.policyStatus }}</span><span>{{ apnServiceUsageLabel(pcc.serviceUsageState) }}</span></div><div class="empty-inline" v-if="!(apn.pccs || []).length">暂无 PCC 策略</div></div></div><div class="empty-inline" v-if="!(selectedCardDetail.apns || []).length">暂无 APN 诊断数据</div></div></div></section></div>
|
||||
<div class="modal-backdrop" v-if="selectedOrderDetail" @click.self="selectedOrderDetail = null"><section class="detail-modal compact"><div class="detail-head"><h2>订单完整信息</h2><button class="ghost icon-only" @click="selectedOrderDetail = null" title="关闭"><X :size="18" /></button></div><div class="detail-grid"><div class="detail-item" v-for="key in ['orderNum','msisdn','iccid','imsi','type','operType','apnName','status','createdAt']" :key="key"><span>{{ key }}</span><strong class="mono">{{ selectedOrderDetail[key] || '-' }}</strong></div></div></section></div>
|
||||
<div class="modal-backdrop" v-if="isCardEditorOpen" @click.self="isCardEditorOpen = false"><section class="detail-modal card-editor-modal">
|
||||
<div class="detail-head"><div><h2>{{ cardEditorMode === 'edit' ? '编辑卡' : '新增卡' }}</h2><p>保存后将刷新当前卡列表。</p></div><button class="ghost icon-only" @click="isCardEditorOpen = false" title="关闭"><X :size="18" /></button></div>
|
||||
<div class="editor-section"><h3>基础信息</h3><div class="editor-grid"><label><span>MSISDN</span><input v-model="cardForm.msisdn" /></label><label><span>ICCID</span><input v-model="cardForm.iccid" /></label><label><span>IMSI</span><input v-model="cardForm.imsi" /></label><label><span>IMEI</span><input v-model="cardForm.imei" /></label><label class="wide"><span>备注</span><input v-model="cardForm.remark" /></label></div></div>
|
||||
<div class="editor-section"><h3>状态与流量</h3><div class="editor-grid"><label><span>卡状态</span><select v-model="cardForm.cardStatus"><option value="1">待激活</option><option value="2">已激活</option><option value="4">停机</option><option value="6">可测试</option><option value="7">库存</option><option value="8">预销户</option></select></label><label><span>终端开关机</span><select v-model="cardForm.onOffStatus"><option value="0">关机</option><option value="1">开机</option></select></label><label><span>会话状态</span><select v-model="cardForm.sessionStatus"><option value="00">离线</option><option value="01">在线</option></select></label><label><span>机卡绑定</span><select v-model="cardForm.cardBindStatus"><option value="0">已分离</option><option value="1">未分离</option></select></label><label><span>区域限制</span><select v-model="cardForm.regionLimitStatus"><option value="0">未触发</option><option value="1">已触发</option></select></label><label><span>当月流量 KB</span><input v-model.number="cardForm.monthUsageKB" type="number" /></label><label><span>日基础流量 KB</span><input v-model.number="cardForm.dailyBaseKB" type="number" /></label><label><span>月基础流量 KB</span><input v-model.number="cardForm.monthlyBaseKB" type="number" /></label></div></div>
|
||||
<div class="editor-section"><h3>诊断模拟</h3><div class="editor-grid"><label><span>停机原因码</span><input v-model="cardForm.stopReason" maxlength="12" /></label><label><span>管理冻结</span><select v-model="cardForm.frozenStatus"><option value="0">否</option><option value="1">是</option></select></label><label class="wide"><span>冻结原因</span><input v-model="cardForm.frozenReason" /></label><label><span>APN 诊断状态</span><select v-model="cardForm.apns[0].diagnosisStatus"><option value="1">未激活</option><option value="2">激活</option><option value="4">暂停</option></select></label><label><span>APN 限速档位</span><input v-model="cardForm.apns[0].serviceUsageState" /></label><label class="wide"><span>超停原因</span><input v-model="cardForm.apns[0].overUseDetails" placeholder="例如 16|17" /></label><label><span>PCC 编码</span><input v-model="cardForm.apns[0].pccs[0].serviceCode" /></label><label><span>PCC 名称</span><input v-model="cardForm.apns[0].pccs[0].name" /></label><label><span>PCC 策略状态</span><select v-model="cardForm.apns[0].pccs[0].policyStatus"><option value="2">激活</option><option value="4">暂停</option></select></label></div></div>
|
||||
<div class="editor-actions"><button class="ghost" @click="isCardEditorOpen = false">取消</button><button :disabled="cardSaving" @click="saveCard"><Save :size="16" />{{ cardSaving ? '保存中...' : '保存卡' }}</button></div>
|
||||
</section></div>
|
||||
<div class="modal-backdrop" v-if="isGroupEditorOpen" @click.self="isGroupEditorOpen = false"><section class="detail-modal compact"><div class="detail-head"><h2>新建分组</h2><button class="ghost icon-only" @click="isGroupEditorOpen = false" title="关闭"><X :size="18" /></button></div><label class="field"><span>分组名称</span><input v-model="groupForm.name" @keyup.enter="createGroup" /></label><div class="editor-actions"><button class="ghost" @click="isGroupEditorOpen = false">取消</button><button :disabled="groupSaving || !groupForm.name.trim()" @click="createGroup"><Save :size="16" />{{ groupSaving ? '保存中...' : '创建分组' }}</button></div></section></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<header class="page-header"><div><button class="ghost icon-only" @click="navigate('dashboard')" title="返回管理台"><ArrowLeft :size="18" /></button><div><p class="eyebrow">OneLink V25.11.1 CT Mock</p><h1>操作日志</h1></div></div><span>每日 00:00 自动清空</span></header>
|
||||
<section class="band log-page">
|
||||
<div class="section-head"><div><h2>今日操作</h2><p>记录管理台、OneLink 和 Chan 接口的操作结果。</p></div><ClipboardList :size="22" /></div>
|
||||
<div class="log-search-row"><input v-model="operationLogKeyword" @keyup.enter="searchOperationLogs" placeholder="搜索动作、目标、接口或 IP" /><select v-model="operationLogCategory" @change="searchOperationLogs"><option value="">全部类别</option><option value="管理台">管理台</option><option value="OneLink">OneLink</option><option value="Chan">Chan</option></select><select v-model="operationLogResult" @change="searchOperationLogs"><option value="">全部结果</option><option value="success">成功</option><option value="failed">失败</option></select><button @click="searchOperationLogs"><Search :size="16" />查询</button><button class="ghost icon-only" @click="clearOperationLogSearch" title="清空查询"><X :size="16" /></button></div>
|
||||
<div class="empty" v-if="!operationLogs.length">暂无操作日志</div>
|
||||
<div class="table log-table" v-else>
|
||||
<div class="tr th"><span>时间</span><span>类别</span><span>动作</span><span>API 接口</span><span>目标</span><span>结果</span><span>耗时</span><span>客户端 IP</span><span></span></div>
|
||||
<div class="tr" v-for="log in operationLogs" :key="log.id"><span>{{ log.createdAt }}</span><span>{{ log.category }}</span><span>{{ log.action }}</span><span class="mono api-path">{{ log.method }} {{ log.path }}</span><span class="mono">{{ log.target || '-' }}</span><span><em class="result-tag" :class="log.result">{{ log.result === 'success' ? '成功' : '失败' }}</em></span><span>{{ log.durationMs }} ms</span><span class="mono">{{ log.clientIp || '-' }}</span><button class="ghost icon-only" @click="selectedOperationLog = log" title="详情"><Eye :size="15" /></button></div>
|
||||
</div>
|
||||
<div class="pager" v-if="operationLogPage.total"><span>共 {{ operationLogPage.total }} 条日志</span><label class="page-size"><span>每页</span><select :value="operationLogPage.pageSize" @change="changeOperationLogPageSize($event.target.value)"><option v-for="size in [10,20,50,100]" :key="size" :value="size">{{ size }} 条</option></select></label><button class="ghost" :disabled="operationLogPage.page <= 1" @click="changeOperationLogPage(operationLogPage.page - 1)">上一页</button><span>第 {{ operationLogPage.page }} / {{ operationLogTotalPages }} 页</span><button class="ghost" :disabled="operationLogPage.page >= operationLogTotalPages" @click="changeOperationLogPage(operationLogPage.page + 1)">下一页</button></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal-backdrop" v-if="selectedOperationLog" @click.self="selectedOperationLog = null"><section class="detail-modal compact"><div class="detail-head"><h2>操作日志详情</h2><button class="ghost icon-only" @click="selectedOperationLog = null" title="关闭"><X :size="18" /></button></div><div class="detail-grid"><div class="detail-item" v-for="key in ['createdAt','category','action','target','method','path','params','result','statusCode','message','durationMs','clientIp']" :key="key"><span>{{ key }}</span><strong class="mono">{{ selectedOperationLog[key] ?? '-' }}</strong></div></div></section></div>
|
||||
<div class="toast" v-if="message">{{ message }}</div>
|
||||
</main>
|
||||
`
|
||||
}
|
||||
|
||||
function emptyCard() {
|
||||
return {
|
||||
msisdn: '', iccid: '', imsi: '', imei: '', remark: '',
|
||||
stopReason: '000000000000', frozenStatus: '0', frozenReason: '', frozenOperTime: '', sessionCreateDate: '',
|
||||
cardStatus: '2', onOffStatus: '1', sessionStatus: '01', cardBindStatus: '1', regionLimitStatus: '0',
|
||||
monthUsageKB: 18432, dailyBaseKB: 614.4, monthlyBaseKB: 18432, growthPerMinuteKB: 0,
|
||||
apns: [{ name: 'CMIOT', status: '1', diagnosisStatus: '2', serviceUsageState: '1', overUseDetails: '', usageBytes: 0, modifyTime: '', pccs: [{ serviceCode: 'default', name: 'APN 缺省流量', policyStatus: '2', serviceUsageState: '1', modifyTime: '' }] }]
|
||||
}
|
||||
}
|
||||
function transid(appid) { return `${appid}${new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14)}00000001` }
|
||||
function yesterday() { return new Date(Date.now() - 86400000).toISOString().slice(0, 10).replaceAll('-', '') }
|
||||
function monthText() { return new Date().toISOString().slice(0, 7).replace('-', '') }
|
||||
function normalizeBase(base) {
|
||||
const clean = (base || '/').replace(/\/+$/, '')
|
||||
return clean === '' ? '' : clean
|
||||
}
|
||||
function withBase(path) {
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${appBase}${cleanPath}` || '/'
|
||||
}
|
||||
function viewFromPath() {
|
||||
const path = location.pathname.replace(appBase, '') || '/'
|
||||
return path === '/operation-logs' || path === '/operation-logs/' ? 'operation-logs' : 'dashboard'
|
||||
}
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,308 @@
|
||||
:root {
|
||||
font-family: Inter, "Microsoft YaHei", system-ui, sans-serif;
|
||||
color: #1d252b;
|
||||
background: #f5f7f8;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; }
|
||||
button, input, select { font: inherit; }
|
||||
|
||||
.shell {
|
||||
width: min(1280px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 24px 0 44px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.count-input {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.group-select {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #57717f;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1, h2, p { margin: 0; }
|
||||
h1 { font-size: clamp(28px, 4vw, 44px); line-height: 1.08; color: #14242d; }
|
||||
h2 { font-size: 20px; }
|
||||
p { color: #63717a; }
|
||||
|
||||
.icon-btn, button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 38px;
|
||||
padding: 0 14px;
|
||||
background: #116a6d;
|
||||
color: white;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn { width: 42px; padding: 0; }
|
||||
.ghost {
|
||||
min-height: 30px;
|
||||
color: #116a6d;
|
||||
background: #e5f0ef;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.metrics div {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: white;
|
||||
border: 1px solid #dce4e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.metric-link { cursor: pointer; }
|
||||
.metric-link:hover, .metric-link:focus-visible { border-color: #116a6d; background: #f0f8f7; outline: none; }
|
||||
|
||||
.metrics strong { font-size: 26px; color: #0f4348; }
|
||||
.metrics span { color: #68777f; }
|
||||
|
||||
.band {
|
||||
background: white;
|
||||
border: 1px solid #dce4e6;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-head-actions { display: inline-flex; align-items: center; gap: 14px; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.search-row { display: grid; grid-template-columns: minmax(260px, 1fr) 150px 150px auto 42px; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
.search-row > button { white-space: nowrap; }
|
||||
|
||||
.app-grid { grid-template-columns: 1.4fr 1fr 1fr auto; }
|
||||
.card-grid { grid-template-columns: repeat(4, minmax(0, 1fr)) auto; }
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid #ccd7da;
|
||||
border-radius: 7px;
|
||||
padding: 8px 10px;
|
||||
color: #1d252b;
|
||||
background: #fbfcfc;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 1px solid #e0e7e9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.4fr 2fr 56px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e8eeee;
|
||||
}
|
||||
|
||||
.tr:first-child { border-top: 0; }
|
||||
.th {
|
||||
background: #f1f5f5;
|
||||
color: #52646d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cards .tr {
|
||||
grid-template-columns: 42px 1fr 1.5fr 1.25fr 0.85fr 0.7fr 0.8fr 0.6fr 0.7fr 150px;
|
||||
}
|
||||
.cards { max-height: 492px; overflow-y: auto; }
|
||||
.cards .tr.th { position: sticky; top: 0; z-index: 1; }
|
||||
.orders { max-height: 492px; overflow-y: auto; }
|
||||
.orders .tr.th { position: sticky; top: 0; z-index: 1; }
|
||||
|
||||
.row-actions { display: inline-flex; gap: 8px; justify-content: flex-end; }
|
||||
.export-status { display: inline-block; padding: 2px 7px; border-radius: 4px; font-size: 12px; font-style: normal; }
|
||||
.export-status.exported { color: #166534; background: #dcfce7; }
|
||||
.export-status.not-exported { color: #9a3412; background: #ffedd5; }
|
||||
.icon-only { width: 34px; padding: 0; }
|
||||
.pager { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 10px; color: #63717a; font-size: 13px; }
|
||||
.pager button:disabled { cursor: not-allowed; opacity: 0.45; }
|
||||
.page-size { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.page-size select { width: auto; min-height: 30px; padding: 4px 28px 4px 8px; }
|
||||
.page-jump { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.page-input { width: 56px; min-height: 30px; padding: 4px 8px; text-align: center; }
|
||||
.page-input::-webkit-outer-spin-button,
|
||||
.page-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
.page-input[type=number] { -moz-appearance: textfield; }
|
||||
|
||||
.modal-backdrop { position: fixed; inset: 0; z-index: 20; display: grid; place-items: center; padding: 20px; background: rgb(16 28 34 / 55%); }
|
||||
.detail-modal { width: min(920px, 100%); max-height: min(760px, calc(100vh - 40px)); overflow: auto; padding: 20px; border-radius: 8px; background: white; box-shadow: 0 18px 50px rgb(0 0 0 / 20%); }
|
||||
.detail-modal.compact { width: min(720px, 100%); }
|
||||
.card-editor-modal { width: min(860px, 100%); }
|
||||
.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; overflow: hidden; border: 1px solid #dce4e6; border-radius: 8px; background: #dce4e6; }
|
||||
.detail-item { min-width: 0; display: grid; gap: 6px; padding: 12px; background: white; }
|
||||
.detail-item span { color: #63717a; font-size: 12px; }
|
||||
.detail-item strong { overflow-wrap: anywhere; font-weight: 600; }
|
||||
.card-detail-modal { width: min(980px, 100%); }
|
||||
.detail-section { margin-top: 16px; }
|
||||
.detail-section:first-of-type { margin-top: 0; }
|
||||
.detail-section h3 { margin: 0 0 9px; color: #31404a; font-size: 14px; }
|
||||
.wrap-value { white-space: pre-wrap; }
|
||||
.diagnosis-list { display: grid; gap: 10px; }
|
||||
.diagnosis-row { padding: 12px; border: 1px solid #dce4e6; border-radius: 7px; background: #fbfcfc; }
|
||||
.diagnosis-row-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.diagnosis-row-head span { padding: 3px 7px; border-radius: 4px; color: #116a6d; background: #e5f0ef; font-size: 12px; }
|
||||
.diagnosis-meta { margin-top: 7px; color: #63717a; font-size: 12px; }
|
||||
.pcc-list { display: grid; gap: 6px; margin-top: 10px; }
|
||||
.pcc-item { display: grid; grid-template-columns: 1fr 1.5fr 0.8fr 1fr; gap: 8px; padding: 7px 9px; border-left: 3px solid #9fc7c6; color: #40515a; background: #f1f5f5; font-size: 12px; }
|
||||
.empty-inline { color: #71818a; font-size: 12px; }
|
||||
.editor-section { padding: 14px 0; border-top: 1px solid #e1e8e9; }
|
||||
.editor-section:first-of-type { border-top: 0; }
|
||||
.editor-section h3 { margin: 0 0 10px; font-size: 14px; color: #31404a; }
|
||||
.editor-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
||||
.editor-grid label { display: grid; gap: 6px; min-width: 0; }
|
||||
.editor-grid label span { color: #63717a; font-size: 12px; }
|
||||
.editor-grid .wide { grid-column: span 2; }
|
||||
.editor-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 16px; }
|
||||
.editor-actions button:disabled { cursor: wait; opacity: 0.55; }
|
||||
.order-row, .table .th:has(+ .order-row) { grid-template-columns: 1.5fr 1fr 0.6fr 1fr 0.7fr 1.2fr 42px; }
|
||||
|
||||
.select-box {
|
||||
width: 18px;
|
||||
min-height: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 0.8fr 1.2fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.split.single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: #31404a;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: #52646d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.copy-field { display: grid; grid-template-columns: minmax(0, 1fr) 42px; gap: 8px; align-items: center; }
|
||||
.copy-field input { min-width: 0; }
|
||||
.endpoint-select { max-height: 220px; overflow-y: auto; }
|
||||
.endpoint-body { width: 100%; min-height: 150px; resize: vertical; border: 1px solid #ccd7da; border-radius: 7px; padding: 10px; color: #1d252b; background: #fbfcfc; }
|
||||
.endpoint-body.compact { min-height: 112px; }
|
||||
.endpoint-detail { margin-top: 12px; }
|
||||
.endpoint-categories { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
||||
.endpoint-category { min-height: 38px; padding: 8px 12px; color: #40515a; background: #f4f7f7; border: 1px solid #ccd7da; }
|
||||
.endpoint-category span { min-width: 22px; height: 20px; display: inline-grid; place-items: center; margin-left: 7px; border-radius: 10px; color: #63717a; background: #e4e9ea; font-size: 12px; }
|
||||
.endpoint-category.active { color: #fff; background: #116a6d; border-color: #116a6d; }
|
||||
.endpoint-category.active span { color: #116a6d; background: #fff; }
|
||||
|
||||
.empty {
|
||||
min-height: 64px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #71818a;
|
||||
border: 1px dashed #cbd7da;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: #14242d;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 18px; }
|
||||
.page-header > div { display: flex; align-items: center; gap: 12px; }
|
||||
.page-header > span { color: #63717a; font-size: 13px; }
|
||||
.log-search-row { display: grid; grid-template-columns: minmax(260px, 1fr) 140px 140px auto 42px; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
.log-table { overflow-x: auto; }
|
||||
.log-table .tr { min-width: 1240px; grid-template-columns: 1.15fr 0.65fr 1.25fr 1.7fr 1.2fr 0.65fr 0.6fr 1fr 42px; }
|
||||
.api-path { overflow-wrap: anywhere; }
|
||||
.result-tag { font-style: normal; font-size: 12px; color: #116a6d; background: #e5f0ef; padding: 4px 8px; border-radius: 6px; }
|
||||
.result-tag.failed { color: #a23b32; background: #fae9e7; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.metrics, .split, .app-grid, .card-grid, .search-row, .log-search-row, .detail-grid, .editor-grid { grid-template-columns: 1fr; }
|
||||
.editor-grid .wide { grid-column: auto; }
|
||||
.topbar { align-items: flex-start; flex-direction: column; }
|
||||
.top-actions { justify-content: flex-start; }
|
||||
.tr, .cards .tr { grid-template-columns: 1fr; align-items: start; }
|
||||
.th { display: none; }
|
||||
.endpoint-categories { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pcc-item { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/v5': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/ec': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/sim': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/ct': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user