mirror of
https://github.com/rekryt/iplist.git
synced 2025-10-12 08:34:15 +03:00
feat: new frontend
This commit is contained in:
6
frontend/.dockerignore
Normal file
6
frontend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.nuxt
|
||||
.output
|
||||
node_modules
|
||||
tmp
|
||||
package-lock.json
|
43
frontend/.editorconfig
Normal file
43
frontend/.editorconfig
Normal file
@@ -0,0 +1,43 @@
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=false
|
||||
indent_style=space
|
||||
tab_width=4
|
||||
|
||||
[*.vue]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.scss]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.json]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{phpunit.xml.dist,*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.svg]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[.editorconfig]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.gy,*.groovy,*.gant,*.gdsl}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.ats,*.ts}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.yml,*.yaml}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
|
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
API_BASE_URL="https://iplist.opencck.org/"
|
24
frontend/.eslintrc.js
Normal file
24
frontend/.eslintrc.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:nuxt/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
rules: {
|
||||
'vue/html-indent': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
};
|
133
frontend/.gitignore
vendored
Normal file
133
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.output
|
||||
.nitro
|
||||
jspm_packages
|
||||
vendor
|
||||
|
||||
# Only keep yarn.lock in the root
|
||||
package-lock.json
|
||||
*/**/yarn.lock
|
||||
composer.lock
|
||||
|
||||
# Logs
|
||||
/logs
|
||||
*.log*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Cache
|
||||
.phpunit.result.cache
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Packages
|
||||
packages/*/LICENSE
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Intellij idea
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# Zend \ Eclipse
|
||||
/.settings/
|
||||
/.buildpath
|
||||
/.project
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
CHANGELOG.md
|
||||
|
||||
TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
|
||||
# Robots Files
|
||||
robots.txt
|
||||
|
||||
# Ignore Service Worker
|
||||
sw.*
|
14
frontend/.prettierrc
Normal file
14
frontend/.prettierrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"phpVersion": "7.1",
|
||||
"braceStyle": "1tbs",
|
||||
"endOfLine": "auto"
|
||||
}
|
12
frontend/README.md
Normal file
12
frontend/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Nuxt + Vuetify frontend for IPList
|
||||
|
||||
## Develop
|
||||
```shell
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build
|
||||
```shell
|
||||
npm run generate
|
||||
```
|
3
frontend/assets/scss/default.scss
Normal file
3
frontend/assets/scss/default.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
min-width: 320px;
|
||||
}
|
475
frontend/components/base/Form.vue
Normal file
475
frontend/components/base/Form.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<script lang="ts" setup>
|
||||
interface Item {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Group {
|
||||
label: string;
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
|
||||
const { data, pending } = useFetch((process.env.NODE_ENV === 'production' ? '/' : '/api') + '?format=json&data=group', {
|
||||
lazy: true,
|
||||
server: false,
|
||||
default: () => [],
|
||||
});
|
||||
const selected = ref<Item[]>([]);
|
||||
const selectedGroups = ref<Group[]>([]);
|
||||
const selectedExcluded = ref<Item[]>([]);
|
||||
const selectedExcludedGroups = ref<Group[]>([]);
|
||||
const selectedExcludedCIDR4 = ref<string[]>([]);
|
||||
const selectedExcludedIP4 = ref<string[]>([]);
|
||||
const selectedExcludedDomains = ref<string[]>([]);
|
||||
const selectedExcludedIP6 = ref<string[]>([]);
|
||||
const selectedExcludedCIDR6 = ref<string[]>([]);
|
||||
const isWildCard = ref(false);
|
||||
const isFileSave = ref(false);
|
||||
|
||||
const items = computed<Group[]>(() => {
|
||||
return Object.entries(data.value as Record<string, string[]>).reduce<Group[]>((acc, [site, group]) => {
|
||||
let groupObj = acc.find((g) => g.label === group);
|
||||
if (!groupObj) {
|
||||
groupObj = { label: group, items: [] } as Group;
|
||||
acc.push(groupObj);
|
||||
}
|
||||
groupObj.items.push({ label: site, value: site });
|
||||
return acc;
|
||||
}, []);
|
||||
});
|
||||
|
||||
const itemsList = computed(() => {
|
||||
return selectedGroups.value.length > 0
|
||||
? items.value.filter((item) => selectedGroups.value.includes(item.label))
|
||||
: items.value;
|
||||
});
|
||||
|
||||
const itemsExcludedList = computed(() => {
|
||||
return selectedExcludedGroups.value.length > 0
|
||||
? items.value.filter((item) => !selectedExcludedGroups.value.includes(item.label))
|
||||
: items.value;
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
const formatList = ref([
|
||||
{ label: 'JSON', value: 'json' },
|
||||
{ label: 'Text', value: 'text', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'Comma', value: 'comma', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'MikroTik Script', value: 'mikrotik', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'SwitchyOmega RuleList', value: 'switchy', dataTypes: ['domains'] },
|
||||
{ label: 'Dnsmasq nfset', value: 'nfset', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'Dnsmasq ipset', value: 'ipset', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'ClashX', value: 'clashx', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'Keenetic KVAS', value: 'kvas', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'Keenetic Routes (.bat)', value: 'bat', dataTypes: ['ip4', 'cidr4'] },
|
||||
{ label: 'Amnezia', value: 'amnezia', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
{ label: 'Proxy auto configuration (PAC)', value: 'pac', dataTypes: ['domains', 'cidr4'] },
|
||||
{ label: 'Custom', value: 'custom', dataTypes: ['cidr4', 'ip4', 'domains', 'cidr6', 'ip6'] },
|
||||
]);
|
||||
const selectedFormat = ref('json');
|
||||
const customTemplate = ref('');
|
||||
|
||||
const dataTypeList = ref([
|
||||
{ label: t('allData'), value: '' },
|
||||
{ label: t('ipZones4'), value: 'cidr4' },
|
||||
{ label: t('ipAddresses4'), value: 'ip4' },
|
||||
{ label: t('domains'), value: 'domains' },
|
||||
{ label: t('ipZones6'), value: 'cidr6' },
|
||||
{ label: t('ipAddresses6'), value: 'ip6' },
|
||||
]);
|
||||
const selectedDataType = ref('');
|
||||
const allowedDataTypesList = computed(() => {
|
||||
const format = formatList.value.find((format) => format.value === selectedFormat.value);
|
||||
return dataTypeList.value.filter((dataType) => !format.dataTypes || format.dataTypes?.includes(dataType.value));
|
||||
});
|
||||
watch(selectedFormat, () => {
|
||||
const format = formatList.value.find((format) => format.value === selectedFormat.value);
|
||||
if (format.dataTypes) {
|
||||
if (!format.dataTypes.includes(selectedDataType.value)) {
|
||||
selectedDataType.value = allowedDataTypesList.value[0].value;
|
||||
}
|
||||
}
|
||||
});
|
||||
const tab = ref('portals');
|
||||
const toQueryParams = (params: Record<string, never>): string => {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const key in params) {
|
||||
const value = params[key];
|
||||
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
parts.push(`${key}=${encodeURIComponent(item)}`);
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
for (const subKey in value) {
|
||||
const subValue = value[subKey];
|
||||
if (subValue !== undefined && subValue !== null) {
|
||||
parts.push(`${key}[${encodeURIComponent(subKey)}]=${encodeURIComponent(subValue)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('&');
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const data = {
|
||||
format: selectedFormat.value,
|
||||
};
|
||||
if (selectedDataType.value) {
|
||||
data['data'] = selectedDataType.value;
|
||||
if (selectedDataType.value === 'domains' && isWildCard.value) {
|
||||
data['wildcard'] = '1';
|
||||
}
|
||||
}
|
||||
if (selected.value.length > 0) {
|
||||
data['site'] = selected.value.map((item) => item.label);
|
||||
}
|
||||
if (selectedGroups.value.length > 0) {
|
||||
data['group'] = selectedGroups.value;
|
||||
}
|
||||
if (selectedFormat.value === 'custom') {
|
||||
data['template'] = customTemplate.value;
|
||||
}
|
||||
|
||||
if (selectedExcluded.value.length > 0) {
|
||||
data['exclude[site]'] = selectedExcluded.value.map((item) => item.label);
|
||||
}
|
||||
if (selectedExcludedGroups.value.length > 0) {
|
||||
data['exclude[group]'] = selectedExcludedGroups.value;
|
||||
}
|
||||
if (selectedDataType.value === 'ip4' && selectedExcludedIP4.value.length > 0) {
|
||||
data['exclude[ip4]'] = selectedExcludedIP4.value;
|
||||
}
|
||||
if (selectedDataType.value === 'ip6' && selectedExcludedIP6.value.length > 0) {
|
||||
data['exclude[ip6]'] = selectedExcludedIP6.value;
|
||||
}
|
||||
if (selectedDataType.value === 'cidr4' && selectedExcludedCIDR4.value.length > 0) {
|
||||
data['exclude[cidr4]'] = selectedExcludedCIDR4.value;
|
||||
}
|
||||
if (selectedDataType.value === 'cidr6' && selectedExcludedCIDR6.value.length > 0) {
|
||||
data['exclude[cidr6]'] = selectedExcludedCIDR6.value;
|
||||
}
|
||||
if (selectedDataType.value === 'domains' && selectedExcludedDomains.value.length > 0) {
|
||||
data['exclude[domain]'] = selectedExcludedDomains.value;
|
||||
}
|
||||
|
||||
if (isFileSave.value) {
|
||||
data['filesave'] = '1';
|
||||
}
|
||||
|
||||
window.location.href = '/?' + toQueryParams(data);
|
||||
};
|
||||
</script>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"format": "Format",
|
||||
"dataType": "Data type",
|
||||
"template": "Template",
|
||||
"groupName": "Group name",
|
||||
"siteName": "Portal name",
|
||||
"data": "Selected data",
|
||||
"shortmask": "Subnet mask (short) (for IP and CIDR)",
|
||||
"mask": "Subnet mask (full) (for IP and CIDR)",
|
||||
"portals": "Portals",
|
||||
"groups": "Groups",
|
||||
"exclude": "Exclusions",
|
||||
"portalSelection": "Portal selection",
|
||||
"doNotSelectIfNeedAll": "Do not select if you want to get all",
|
||||
"filteredByGroups": "The set of portals is filtered by the selected groups",
|
||||
"groupSelection": "Group selection",
|
||||
"excludePortals": "Exclude portals",
|
||||
"excludeGroups": "Exclude groups",
|
||||
"excludeIpZones": "Exclude IP zones",
|
||||
"excludeIp": "Exclude IP",
|
||||
"excludeDomains": "Exclude domains",
|
||||
"onlyWildcard": "Only wildcard domains",
|
||||
"saveToFile": "Save as file",
|
||||
"submit": "Submit",
|
||||
"allData": "All data",
|
||||
"ipZones4": "IPv4 zones (CIDR)",
|
||||
"ipAddresses4": "IPv4 addresses",
|
||||
"domains": "Domains",
|
||||
"ipZones6": "IPv6 zones (CIDR)",
|
||||
"ipAddresses6": "IPv6 addresses"
|
||||
},
|
||||
"ru": {
|
||||
"format": "Формат",
|
||||
"dataType": "Тип данных",
|
||||
"template": "Шаблон",
|
||||
"groupName": "Имя группы",
|
||||
"siteName": "Имя портала",
|
||||
"data": "Выбранные данные",
|
||||
"shortmask": "Маска подсети (короткая) (для ip и cidr)",
|
||||
"mask": "Маска подсети (полная) (для ip и cidr)",
|
||||
"portals": "Порталы",
|
||||
"groups": "Группы",
|
||||
"exclude": "Исключения",
|
||||
"portalSelection": "Выбор порталов",
|
||||
"doNotSelectIfNeedAll": "Не выбирайте, если хотите получить все",
|
||||
"filteredByGroups": "Набор порталов отфильтрован по выбранным группам",
|
||||
"groupSelection": "Выбор групп",
|
||||
"excludePortals": "Исключить порталы",
|
||||
"excludeGroups": "Исключить группы",
|
||||
"excludeIpZones": "Исключить IP-зоны",
|
||||
"excludeIp": "Исключить IP",
|
||||
"excludeDomains": "Исключить домены",
|
||||
"onlyWildcard": "Только wildcard домены",
|
||||
"saveToFile": "Сохранить как файл",
|
||||
"submit": "Отправить",
|
||||
"allData": "Все данные",
|
||||
"ipZones4": "IP-зоны ipv4 (CIDR)",
|
||||
"ipAddresses4": "IP-адреса ipv4",
|
||||
"domains": "Домены",
|
||||
"ipZones6": "IP-зоны ipv6 (CIDR)",
|
||||
"ipAddresses6": "IP-адреса ipv6"
|
||||
},
|
||||
"cn": {
|
||||
"format": "格式",
|
||||
"dataType": "数据类型",
|
||||
"template": "模板",
|
||||
"groupName": "分组名称",
|
||||
"siteName": "门户名称",
|
||||
"data": "已选数据",
|
||||
"shortmask": "子网掩码(简写)(用于 IP 和 CIDR)",
|
||||
"mask": "子网掩码(完整)(用于 IP 和 CIDR)",
|
||||
"portals": "门户",
|
||||
"groups": "分组",
|
||||
"exclude": "排除项",
|
||||
"portalSelection": "门户选择",
|
||||
"doNotSelectIfNeedAll": "如果需要全部,请不要选择",
|
||||
"filteredByGroups": "门户集合已根据所选分组进行筛选。",
|
||||
"groupSelection": "分组选择",
|
||||
"excludePortals": "排除门户",
|
||||
"excludeGroups": "排除分组",
|
||||
"excludeIpZones": "排除 IP 区域",
|
||||
"excludeIp": "排除 IP",
|
||||
"excludeDomains": "排除域名",
|
||||
"onlyWildcard": "仅限通配符域名",
|
||||
"saveToFile": "保存为文件",
|
||||
"submit": "提交",
|
||||
"allData": "所有数据",
|
||||
"ipZones4": "IPv4 区域(CIDR)",
|
||||
"ipAddresses4": "IPv4 地址",
|
||||
"domains": "域名",
|
||||
"ipZones6": "IPv6 区域(CIDR)",
|
||||
"ipAddresses6": "IPv6 地址"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<template>
|
||||
<v-form class="baseForm mx-auto">
|
||||
<v-card class="px-4 py-8" elevation="10">
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-select
|
||||
v-model="selectedFormat"
|
||||
:items="formatList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
:label="t('format')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-select
|
||||
v-model="selectedDataType"
|
||||
:items="allowedDataTypesList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
:label="t('dataType')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col v-if="selectedFormat === 'custom'" cols="12">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="customTemplate"
|
||||
:label="t('template')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="auto" class="d-flex flex-column justify-center pl-0">
|
||||
<v-tooltip interactive>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-icon v-bind="activatorProps" color="tertiary">mdi-help</v-icon>
|
||||
</template>
|
||||
<div class="pa-4">
|
||||
<ul>
|
||||
<li>{group} - {{ t('groupName') }}</li>
|
||||
<li>{site} - {{ t('siteName') }}</li>
|
||||
<li>{data} - {{ t('groupName') }}</li>
|
||||
<li>{shortmask} - {{ t('shortmask') }}</li>
|
||||
<li>{mask} - {{ t('mask') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-tabs v-model="tab" bg-color="primary">
|
||||
<v-tab value="portals">{{ t('portals') }}</v-tab>
|
||||
<v-tab value="groups">{{ t('groups') }}</v-tab>
|
||||
<v-tab value="exclude">{{ t('exclude') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-card-text>
|
||||
<v-tabs-window v-model="tab">
|
||||
<v-tabs-window-item class="pt-2" value="portals">
|
||||
<base-form-portals
|
||||
v-model="selected"
|
||||
:label="t('portalSelection')"
|
||||
:items="itemsList"
|
||||
:selected-groups="selectedGroups"
|
||||
:hint="
|
||||
selectedGroups.length === 0
|
||||
? t('doNotSelectIfNeedAll')
|
||||
: t('filteredByGroups')
|
||||
"
|
||||
persistent-hint
|
||||
:loading="pending"
|
||||
></base-form-portals>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item class="pt-2" value="groups">
|
||||
<base-form-groups
|
||||
v-model="selectedGroups"
|
||||
:label="t('groupSelection')"
|
||||
:items="items"
|
||||
:hint="selected.length === 0 ? t('doNotSelectIfNeedAll') : ''"
|
||||
:persistent-hint="selected.length === 0"
|
||||
:loading="pending"
|
||||
></base-form-groups>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item class="pt-2" value="exclude">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<base-form-groups
|
||||
v-model="selectedExcludedGroups"
|
||||
:label="t('excludeGroups')"
|
||||
:items="items"
|
||||
:loading="pending"
|
||||
hide-details
|
||||
></base-form-groups>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<base-form-portals
|
||||
v-model="selectedExcluded"
|
||||
:label="t('excludePortals')"
|
||||
:items="itemsExcludedList"
|
||||
:loading="pending"
|
||||
hide-details
|
||||
></base-form-portals>
|
||||
</v-col>
|
||||
<v-col v-if="selectedDataType === 'cidr4'" cols="12">
|
||||
<v-combobox
|
||||
v-model="selectedExcludedCIDR4"
|
||||
:label="t('excludeIpZones') + ' ipv4'"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col v-if="selectedDataType === 'ip4'" cols="12">
|
||||
<v-combobox
|
||||
v-model="selectedExcludedIP4"
|
||||
:label="t('excludeIp') + ' ipv4'"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col v-if="selectedDataType === 'domains'" cols="12">
|
||||
<v-combobox
|
||||
v-model="selectedExcludedDomains"
|
||||
:label="t('excludeDomains')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col v-if="selectedDataType === 'cidr6'" cols="12">
|
||||
<v-combobox
|
||||
v-model="selectedExcludedCIDR6"
|
||||
:label="t('excludeIpZones') + ' ipv6'"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col v-if="selectedDataType === 'ip6'" cols="12">
|
||||
<v-combobox
|
||||
v-model="selectedExcludedIP6"
|
||||
:label="t('excludeIp') + ' ipv6'"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col class="py-0" cols="12">
|
||||
<v-checkbox
|
||||
v-if="selectedDataType === 'domains'"
|
||||
v-model="isWildCard"
|
||||
:label="t('onlyWildcard')"
|
||||
:value="true"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="isFileSave"
|
||||
:label="t('saveToFile')"
|
||||
:value="true"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn color="primary" block size="50" @click="submit">{{ t('submit') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.baseForm {
|
||||
max-width: 580px;
|
||||
}
|
||||
</style>
|
81
frontend/components/base/form/groups.vue
Normal file
81
frontend/components/base/form/groups.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
persistentHint: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideDetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const selected = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"allGroups": "All groups",
|
||||
"noData": "Not found"
|
||||
},
|
||||
"ru": {
|
||||
"allGroups": "Все группы",
|
||||
"noData": "Не найдено"
|
||||
},
|
||||
"cn": {
|
||||
"allGroups": "所有分组",
|
||||
"noData": "未找到"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
:label="label"
|
||||
item-title="label"
|
||||
item-value="label"
|
||||
variant="outlined"
|
||||
:placeholder="t('allGroups')"
|
||||
:no-data-text="t('noData')"
|
||||
:hint="hint"
|
||||
:persistent-hint="persistentHint"
|
||||
:loading="loading"
|
||||
:hide-details="hideDetails"
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
></v-autocomplete>
|
||||
</template>
|
121
frontend/components/base/form/portals.vue
Normal file
121
frontend/components/base/form/portals.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
persistentHint: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideDetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const selected = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value);
|
||||
},
|
||||
});
|
||||
const setSelect = (subItem: { label: string; value: string }): void => {
|
||||
if (selected.value.includes(subItem as never)) {
|
||||
selected.value.splice(selected.value.indexOf(subItem as never), 1);
|
||||
} else {
|
||||
selected.value.push(subItem);
|
||||
}
|
||||
};
|
||||
const setSelectGroup = (item: { label: string; items: { label: string; value: string }[] }): void => {
|
||||
if (item.items.every((subItem) => selected.value.includes(subItem as never))) {
|
||||
item.items.forEach((subItem) => selected.value.splice(selected.value.indexOf(subItem as never), 1));
|
||||
} else {
|
||||
item.items.forEach((subItem: { label: string; value: string }) => {
|
||||
if (!selected.value.includes(subItem as never)) {
|
||||
selected.value.push(subItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"allPortals": "All portals",
|
||||
"noData": "Not found"
|
||||
},
|
||||
"ru": {
|
||||
"allPortals": "Все порталы",
|
||||
"noData": "Не найдено"
|
||||
},
|
||||
"cn": {
|
||||
"allPortals": "所有门户",
|
||||
"noData": "未找到"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
:label="label"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
item-children="items"
|
||||
variant="outlined"
|
||||
:placeholder="t('allPortals')"
|
||||
:no-data-text="t('noData')"
|
||||
:hint="hint"
|
||||
:persistent-hint="persistentHint"
|
||||
:loading="loading"
|
||||
:hide-details="hideDetails"
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list lines="one" select-strategy="classic">
|
||||
<v-list-item class="font-weight-bold" density="compact" @click="() => setSelectGroup(item.raw)">
|
||||
{{ item.raw.label }}
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="(subItem, index) in item.raw.items"
|
||||
:key="index"
|
||||
:tabindex="index"
|
||||
:value="`nestedList${index}`"
|
||||
:active="selected.includes(subItem)"
|
||||
density="compact"
|
||||
@click="() => setSelect(subItem)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ subItem.label }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
206
frontend/components/core/AppBar.vue
Normal file
206
frontend/components/core/AppBar.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<v-app-bar id="core-app-bar" absolute color="transparent" flat height="88">
|
||||
<v-toolbar-title class="font-weight-light align-self-center text-no-wrap">
|
||||
<v-btn v-show="!responsive" icon @click.stop="onClick">
|
||||
<v-icon>mdi-view-list</v-icon>
|
||||
</v-btn>
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-toolbar-items class="flex-fill">
|
||||
<v-row align="center" justify="end" class="mx-0 px-4">
|
||||
<v-col class="px-0 d-block d-md-none" cols="auto">
|
||||
<github-button
|
||||
class="d-block mt-1"
|
||||
href="https://github.com/rekryt/iplist"
|
||||
:data-color-scheme="theme.name.value"
|
||||
data-icon="octicon-star"
|
||||
data-size="small"
|
||||
aria-label="Star rekryt/iplist on GitHub"
|
||||
>
|
||||
Star
|
||||
</github-button>
|
||||
</v-col>
|
||||
<v-col class="d-none d-md-block" cols="auto">
|
||||
<github-button
|
||||
class="d-block mt-1"
|
||||
href="https://github.com/rekryt/iplist"
|
||||
:data-color-scheme="theme.name.value"
|
||||
data-icon="octicon-star"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star rekryt/iplist on GitHub"
|
||||
>
|
||||
Star
|
||||
</github-button>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="locale"
|
||||
:items="localesList"
|
||||
item-title="label"
|
||||
item-value="code"
|
||||
:label="t('language')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="w-32"
|
||||
hide-details
|
||||
@update:model-value="setLocale"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar size="20" class="mr-2">
|
||||
<img :src="item.raw.flag" alt="flag" />
|
||||
</v-avatar>
|
||||
<span class="d-none d-md-block">{{ item.raw.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{ item, props }">
|
||||
<v-list-item v-bind="props">
|
||||
<template #prepend>
|
||||
<v-avatar size="20">
|
||||
<img :src="item.raw.flag" alt="flag" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-btn height="48" icon @click="toggleTheme">
|
||||
<v-icon color="tertiary">mdi-theme-light-dark</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"language": "Language",
|
||||
"index___en": "Portals",
|
||||
"about___en": "About",
|
||||
"groups___en": "Groups"
|
||||
},
|
||||
"ru": {
|
||||
"language": "Язык",
|
||||
"index___ru": "Порталы",
|
||||
"about___ru": "О проекте",
|
||||
"groups___ru": "Группы"
|
||||
},
|
||||
"cn": {
|
||||
"language": "语言",
|
||||
"index___cn": "通过门户",
|
||||
"about___cn": "关于项目",
|
||||
"groups___cn": "分组"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<script>
|
||||
// Utilities
|
||||
import { mapActions } from 'pinia';
|
||||
import { useAppStore } from '~/stores/app';
|
||||
import { useDisplay, useTheme } from 'vuetify';
|
||||
import GithubButton from 'vue-github-button';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GithubButton,
|
||||
},
|
||||
setup() {
|
||||
const { t, locale, locales } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
const theme = useTheme();
|
||||
const cookieTheme = useCookieTheme();
|
||||
const localePath = useLocalePath();
|
||||
const router = useRouter();
|
||||
const localsData = [
|
||||
{ code: 'en', language: 'English', flag: 'https://flagcdn.com/w40/us.png' },
|
||||
{ code: 'ru', language: 'Русский', flag: 'https://flagcdn.com/w40/ru.png' },
|
||||
{ code: 'cn', language: '简体中文', flag: 'https://flagcdn.com/w40/cn.png' },
|
||||
];
|
||||
const localesList = computed(() =>
|
||||
locales.value.map((l) => ({
|
||||
value: l,
|
||||
code: l.code,
|
||||
label: localsData.find((d) => d.code === l.code).language,
|
||||
flag: localsData.find((d) => d.code === l.code).flag,
|
||||
}))
|
||||
);
|
||||
const localeData = computed(() => localsData.find((l) => l.code === locale.value));
|
||||
|
||||
const setLocale = (value) => {
|
||||
router.push(localePath(router.currentRoute.value.path, value));
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const title = computed(() => {
|
||||
return t(route.name);
|
||||
});
|
||||
|
||||
const toggleTheme = () => {
|
||||
const themeValue = theme.global.current.value.dark ? 'light' : 'dark';
|
||||
theme.global.name.value = themeValue;
|
||||
cookieTheme.value = themeValue;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
toggleTheme();
|
||||
await nextTick();
|
||||
toggleTheme();
|
||||
});
|
||||
|
||||
return {
|
||||
theme,
|
||||
t,
|
||||
locale,
|
||||
localesList,
|
||||
setLocale,
|
||||
localeData,
|
||||
title,
|
||||
toggleTheme,
|
||||
};
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
notifications: [
|
||||
'Mike, John responded to your email',
|
||||
'You have 5 new tasks',
|
||||
"You're now a friend with Andrew",
|
||||
'Another Notification',
|
||||
'Another One',
|
||||
],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
responsive() {
|
||||
const display = useDisplay();
|
||||
return display.lgAndUp.value;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.setDrawer(this.responsive);
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(useAppStore, ['setDrawer', 'toggleDrawer']),
|
||||
onClick() {
|
||||
this.setDrawer(!useAppStore().drawer);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Fix coming in v2.0.8 */
|
||||
#core-app-bar {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#core-app-bar a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
94
frontend/components/core/Drawer.vue
Normal file
94
frontend/components/core/Drawer.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<v-navigation-drawer id="app-drawer" v-model="inputValue" width="260" elevation="5" floating rail>
|
||||
<v-row justify="center" class="text-center">
|
||||
<v-col class="pt-8">
|
||||
<v-avatar color="white">
|
||||
<v-img src="/icon.png" height="34" contain />
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mx-3 mb-3" />
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item v-for="(link, i) in links" :key="i" :to="link.to" active-class="primary white--text">
|
||||
<template #prepend>
|
||||
<v-icon>{{ link.icon }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ link.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template #append>
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item tag="a" href="https://github.com/rekryt/iplist" target="_blank">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-github</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-light">GitHub</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"main": "Portals",
|
||||
"groups": "Groups",
|
||||
"about": "About"
|
||||
},
|
||||
"ru": {
|
||||
"main": "Порталы",
|
||||
"groups": "Группы",
|
||||
"about": "О проекте"
|
||||
},
|
||||
"cn": {
|
||||
"main": "通过门户",
|
||||
"groups": "分组",
|
||||
"about": "关于项目"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import { useAppStore } from '~/stores/app';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
const localePath = useLocalePath();
|
||||
const links = computed(() => [
|
||||
{
|
||||
to: localePath('/'),
|
||||
icon: 'mdi-web-sync',
|
||||
text: t('main'),
|
||||
},
|
||||
{
|
||||
to: localePath('/about'),
|
||||
icon: 'mdi-information-outline',
|
||||
text: t('about'),
|
||||
},
|
||||
]);
|
||||
|
||||
return { t, links };
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAppStore, ['color']),
|
||||
inputValue: {
|
||||
get() {
|
||||
return useAppStore().drawer;
|
||||
},
|
||||
set(val) {
|
||||
this.setDrawer(val);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(useAppStore, ['setDrawer', 'toggleDrawer']),
|
||||
},
|
||||
};
|
||||
</script>
|
56
frontend/components/core/Footer.vue
Normal file
56
frontend/components/core/Footer.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
const links = computed(() => [{ name: t('issue'), Link: 'https://github.com/rekryt/iplist/issues' }]);
|
||||
</script>
|
||||
<template>
|
||||
<v-footer id="core-footer">
|
||||
<div class="footer-items">
|
||||
<a v-for="link in links" :key="link.name" :href="link.Link" class="footer-link">
|
||||
{{ link.name }}
|
||||
</a>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<span class="font-weight-light copyright">
|
||||
© {{ new Date().getFullYear() }}
|
||||
<a href="https://vk.com/rekryt" target="_blank">Rekryt</a>
|
||||
<v-icon style="margin-top: -3px" color="tertiary" size="17">mdi-star</v-icon>
|
||||
for a better web
|
||||
<br />
|
||||
</span>
|
||||
</v-footer>
|
||||
</template>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"issue": "Issue"
|
||||
},
|
||||
"ru": {
|
||||
"issue": "Задать вопрос"
|
||||
},
|
||||
"cn": {
|
||||
"issue": "提交问题"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
<style lang="scss">
|
||||
#core-footer {
|
||||
flex: 0 0 auto;
|
||||
z-index: 0;
|
||||
margin-top: auto;
|
||||
height: 100px;
|
||||
}
|
||||
.copyright {
|
||||
font-size: 14px;
|
||||
}
|
||||
.footer-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.footer-link {
|
||||
margin: 5px 5px 5px 0;
|
||||
}
|
||||
</style>
|
25
frontend/components/core/View.vue
Normal file
25
frontend/components/core/View.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-main class="grey lighten-3">
|
||||
<v-fade-transition mode="out-in">
|
||||
<div id="core-view">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CoreView',
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
body {
|
||||
min-width: 320px;
|
||||
}
|
||||
#core-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
93
frontend/components/material/Card.vue
Normal file
93
frontend/components/material/Card.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<v-card class="v-card--material-card" :style="styles" v-bind="$attrs">
|
||||
<helper-offset v-if="hasOffset" :inline="inline" :full-width="fullWidth" :offset="offset">
|
||||
<v-card
|
||||
v-if="!$slots.offset"
|
||||
:color="color"
|
||||
:elevation="elevation"
|
||||
class="v-card--material__header d-flex align-center"
|
||||
min-height="80"
|
||||
>
|
||||
<slot v-if="!title && !text" name="header" />
|
||||
<div v-else class="px-3">
|
||||
<h4 class="title font-weight-light mb-2" v-text="title" />
|
||||
<p class="category font-weight-thin mb-0" v-text="text" />
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<slot v-else name="offset" />
|
||||
</helper-offset>
|
||||
|
||||
<v-card-text class="cardText">
|
||||
<slot />
|
||||
</v-card-text>
|
||||
|
||||
<v-divider v-if="$slots.actions" class="mx-3" />
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MaterialCard',
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: 'secondary',
|
||||
},
|
||||
elevation: {
|
||||
type: [Number, String],
|
||||
default: 10,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
offset: {
|
||||
type: [Number, String],
|
||||
default: 24,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasOffset() {
|
||||
return this.$slots.header || this.$slots.offset || this.title || this.text;
|
||||
},
|
||||
styles() {
|
||||
if (!this.hasOffset) return null;
|
||||
|
||||
return {
|
||||
marginBottom: `${this.offset}px`,
|
||||
marginTop: `${this.offset * 2}px`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.v-card--material-card {
|
||||
overflow: visible;
|
||||
}
|
||||
.cardText {
|
||||
font-size: 18px !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
</style>
|
6
frontend/composables/useCookieTheme.ts
Normal file
6
frontend/composables/useCookieTheme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function useCookieTheme() {
|
||||
const theme = useCookie<string>('theme');
|
||||
if (!theme.value) theme.value = 'dark';
|
||||
|
||||
return theme;
|
||||
}
|
10
frontend/layouts/default.vue
Normal file
10
frontend/layouts/default.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<core-drawer />
|
||||
<core-app-bar />
|
||||
<core-view>
|
||||
<slot></slot>
|
||||
<core-footer />
|
||||
</core-view>
|
||||
</v-app>
|
||||
</template>
|
101
frontend/nuxt.config.ts
Normal file
101
frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import eslintPlugin from 'vite-plugin-eslint';
|
||||
const path = require('path');
|
||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
typescript: {
|
||||
strict: true,
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
title: 'IP Address Collection and Management Service',
|
||||
meta: [
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
charset: 'utf-8',
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
css: ['vuetify/lib/styles/main.sass', '@mdi/font/css/materialdesignicons.min.css', 'assets/scss/default.scss'],
|
||||
|
||||
modules: [
|
||||
[
|
||||
'@pinia/nuxt',
|
||||
{
|
||||
autoImports: [
|
||||
// automatically imports `defineStore`
|
||||
'defineStore', // import { defineStore } from 'pinia'
|
||||
// automatically imports `defineStore` as `definePiniaStore`
|
||||
['defineStore', 'definePiniaStore'], // import { defineStore as definePiniaStore } from 'pinia'
|
||||
],
|
||||
},
|
||||
],
|
||||
'@kevinmarrec/nuxt-pwa',
|
||||
'@nuxtjs/i18n',
|
||||
],
|
||||
|
||||
pwa: {
|
||||
meta: {
|
||||
name: 'IPList',
|
||||
title: 'IPList',
|
||||
author: 'Rekryt',
|
||||
description: 'IP Address Collection and Management Service with multiple formats',
|
||||
theme_color: '#000000',
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
strategy: 'prefix_except_default',
|
||||
locales: [
|
||||
{ code: 'en', language: 'en-US' },
|
||||
{ code: 'ru', language: 'ru-RU' },
|
||||
{ code: 'cn', language: 'zh-CN' },
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
|
||||
build: {
|
||||
transpile: ['vuetify'],
|
||||
},
|
||||
|
||||
vite: {
|
||||
plugins: [eslintPlugin()],
|
||||
define: {
|
||||
'process.env.DEBUG': false,
|
||||
},
|
||||
server: {
|
||||
/**
|
||||
* If develop from docker
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
|
||||
nitro: {
|
||||
devProxy: {
|
||||
'/api': {
|
||||
target: process.env.API_BASE_URL ?? 'https://iplist.opencck.org/',
|
||||
hostRewrite: process.env.API_BASE_URL ?? 'https://iplist.opencck.org/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
publicDir: path.join(__dirname, '../public/'),
|
||||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2025-06-28',
|
||||
});
|
45
frontend/package.json
Normal file
45
frontend/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "iplist",
|
||||
"version": "1.0.0",
|
||||
"author": "Rekryt <rekrytkw@gmail.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"test": "vitest --run --reporter verbose --globals",
|
||||
"lint": "eslint --ext .ts,.js,.vue ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevinmarrec/nuxt-pwa": "^0.17.0",
|
||||
"@nuxtjs/i18n": "^9.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||
"@typescript-eslint/parser": "8.35.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"consola": "^3.4.2",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-nuxt": "^4.0.0",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"nuxt": "3.17.5",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.89.2",
|
||||
"vite-plugin-eslint": "^1.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@pinia/nuxt": "0.11.1",
|
||||
"css-select": "5.1.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"pinia": "3.0.3",
|
||||
"vue-github-button": "^3.1.3",
|
||||
"vuetify": "3.8.11"
|
||||
},
|
||||
"overrides": {
|
||||
"vue": "latest"
|
||||
}
|
||||
}
|
34
frontend/pages/about.vue
Normal file
34
frontend/pages/about.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
});
|
||||
</script>
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"title": "IP Address Collection and Management Service",
|
||||
"about": "This service is designed to collect and update IP addresses (IPv4 and IPv6), as well as their CIDR zones for specified domains. It is an asynchronous PHP web server based on AMPHP and uses the Linux utilities whois and ipcalc. The service provides interfaces to retrieve lists of IP address zones of specified domains (IPv4 addresses, IPv6 addresses, as well as CIDRv4 and CIDRv6 zones) in various formats, including plain text, JSON, and script formats for importing into \"Address List\" on routers such as MikroTik (RouterOS), Keenetic KVAS\\BAT, SwitchyOmega, Amnezia, and others."
|
||||
},
|
||||
"ru": {
|
||||
"title": "Сервис сбора IP-адресов и CIDR зон",
|
||||
"about": "Данный сервис предназначен для сбора и обновления IP-адресов (IPv4 и IPv6), а также их CIDR зон для указанных доменов. Это асинхронный PHP веб-сервер на основе AMPHP и Linux-утилит whois и ipcalc. Сервис предоставляет интерфейсы для получения списков зон ip адресов указанных доменов (IPv4 адресов, IPv6 адресов, а также CIDRv4 и CIDRv6 зон) в различных форматах, включая текстовый, JSON, форматы скриптов для добавления в \"Address List\" на роутерах Mikrotik (RouterOS), Keenetic KVAS\\BAT, SwitchyOmega, Amnezia и др."
|
||||
},
|
||||
"cn": {
|
||||
"title": "IP地址收集与管理服务",
|
||||
"about": "该服务用于收集和更新指定域名的 IP 地址(IPv4 和 IPv6)及其 CIDR 区段。它是一个基于 AMPHP 的异步 PHP Web 服务器,使用 Linux 工具 whois 和 ipcalc。该服务提供接口,以多种格式(包括纯文本、JSON,以及可用于 MikroTik(RouterOS)、Keenetic KVAS\\BAT、SwitchyOmega、Amnezia 等路由器的“地址列表”导入脚本)获取指定域名的 IP 地址区段(IPv4 地址、IPv6 地址、CIDRv4 和 CIDRv6 区段)列表。"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<div class="max-w-[960px] mx-auto">
|
||||
<h1 class="mb-4">{{ t('title') }}</h1>
|
||||
<material-card>{{ t('about') }}</material-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.max-w-\[960px\] {
|
||||
max-width: 960px;
|
||||
}
|
||||
</style>
|
6
frontend/pages/index.vue
Normal file
6
frontend/pages/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<v-container class="my-auto" fluid>
|
||||
<base-form></base-form>
|
||||
</v-container>
|
||||
</template>
|
65
frontend/plugins/vuetify.ts
Normal file
65
frontend/plugins/vuetify.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createVuetify } from 'vuetify';
|
||||
import * as components from 'vuetify/components';
|
||||
import * as directives from 'vuetify/directives';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const cookieTheme = useCookieTheme();
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: cookieTheme.value && cookieTheme.value !== 'system' ? cookieTheme.value : 'light',
|
||||
themes: {
|
||||
light: {
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: '#4caf50',
|
||||
secondary: '#4caf50',
|
||||
background: '#FFFFFF',
|
||||
surface: '#FFFFFF',
|
||||
'primary-darken-1': '#3700B3',
|
||||
'secondary-darken-1': '#018786',
|
||||
error: '#f55a4e',
|
||||
info: '#00d3ee',
|
||||
success: '#5cb860',
|
||||
warning: '#ffa21a',
|
||||
},
|
||||
},
|
||||
// dark: {
|
||||
// dark: true,
|
||||
// colors: {
|
||||
// background: '#121212',
|
||||
// error: '#CF6679',
|
||||
// info: '#2196F3',
|
||||
// 'on-background': '#fff',
|
||||
// 'on-error': '#fff',
|
||||
// 'on-info': '#fff',
|
||||
// 'on-primary': '#fff',
|
||||
// 'on-primary-darken-1': '#fff',
|
||||
// 'on-secondary': '#fff',
|
||||
// 'on-secondary-darken-1': '#fff',
|
||||
// 'on-success': '#fff',
|
||||
// 'on-surface': '#fff',
|
||||
// 'on-surface-bright': '#000',
|
||||
// 'on-surface-light': '#fff',
|
||||
// 'on-surface-variant': '#000000',
|
||||
// 'on-warning': '#fff',
|
||||
// primary: '#2196F3',
|
||||
// 'primary-darken-1': '#277CC1',
|
||||
// secondary: '#54B6B2',
|
||||
// 'secondary-darken-1': '#48A9A6',
|
||||
// success: '#4CAF50',
|
||||
// surface: '#212121',
|
||||
// 'surface-bright': '#ccbfd6',
|
||||
// 'surface-light': '#424242',
|
||||
// 'surface-variant': '#c8c8c8',
|
||||
// warning: '#FB8C00',
|
||||
// },
|
||||
// },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
nuxtApp.vueApp.use(vuetify);
|
||||
});
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/icon.png
Normal file
BIN
frontend/public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 369 KiB |
81
frontend/public/scripts/homeproxy/update_resources.sh
Normal file
81
frontend/public/scripts/homeproxy/update_resources.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
SERVICE_URL="https://iplist.opencck.org"
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
RESOURCES_DIR="/etc/$NAME/resources"
|
||||
mkdir -p "$RESOURCES_DIR"
|
||||
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
LOG_PATH="$RUN_DIR/$NAME.log"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
set_lock() {
|
||||
local act="$1"
|
||||
local type="$2"
|
||||
|
||||
local lock="$RUN_DIR/update_resources-$type.lock"
|
||||
if [ "$act" = "set" ]; then
|
||||
if [ -e "$lock" ]; then
|
||||
log "[$(to_upper "$type")] A task is already running."
|
||||
exit 2
|
||||
else
|
||||
touch "$lock"
|
||||
fi
|
||||
elif [ "$act" = "remove" ]; then
|
||||
rm -f "$lock"
|
||||
fi
|
||||
}
|
||||
|
||||
to_upper() {
|
||||
echo -e "$1" | tr "[a-z]" "[A-Z]"
|
||||
}
|
||||
|
||||
check_list_update() {
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
local wget="wget --timeout=10 -q"
|
||||
|
||||
set_lock "set" "$listtype"
|
||||
|
||||
$wget "$listrepo/?format=text&data=$listref" -O "$RUN_DIR/$listname"
|
||||
if [ ! -s "$RUN_DIR/$listname" ]; then
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$listrepo/?format=text&data=$listref] Update failed."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
|
||||
echo -e "$(date +%F\ %H:%M:%S)" > "$RESOURCES_DIR/$listtype.ver"
|
||||
log "[$listrepo/?format=text&data=$listref] Successfully updated."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"china_ip4")
|
||||
check_list_update "$1" "$SERVICE_URL" "cidr4" "ipv4.txt"
|
||||
;;
|
||||
"china_ip6")
|
||||
check_list_update "$1" "$SERVICE_URL" "cidr6" "ipv6.txt"
|
||||
;;
|
||||
"gfw_list")
|
||||
check_list_update "$1" "$SERVICE_URL" "domains" "gfw.txt"
|
||||
;;
|
||||
"china_list")
|
||||
check_list_update "$1" "$SERVICE_URL" "domains" "direct-list.txt"
|
||||
;;
|
||||
*)
|
||||
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
17
frontend/stores/app.ts
Normal file
17
frontend/stores/app.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
drawer: false,
|
||||
color: 'success',
|
||||
}),
|
||||
actions: {
|
||||
setDrawer(value: boolean) {
|
||||
this.drawer = value;
|
||||
},
|
||||
setColor(value: string) {
|
||||
this.color = value;
|
||||
},
|
||||
toggleDrawer() {
|
||||
this.drawer = !this.drawer;
|
||||
},
|
||||
},
|
||||
});
|
10
frontend/stores/main.ts
Normal file
10
frontend/stores/main.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const useMainStore = defineStore('main', {
|
||||
state: () => ({
|
||||
sidebar: false,
|
||||
}),
|
||||
actions: {
|
||||
toggleSidebar() {
|
||||
this.sidebar = !this.sidebar;
|
||||
},
|
||||
},
|
||||
});
|
12
frontend/tsconfig.json
Normal file
12
frontend/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
// https://v3.nuxtjs.org/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"rootDir": "./",
|
||||
"moduleResolution": "Node",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user