修改完善这个专利部分

This commit is contained in:
super
2026-05-15 16:06:12 +08:00
parent 18b0c2d211
commit 1dc2cac19a
72 changed files with 2930 additions and 2643 deletions

View File

@@ -5,6 +5,7 @@ import json
import os
import re
import threading
from urllib.parse import quote
import requests
from requests.adapters import HTTPAdapter
@@ -1217,7 +1218,9 @@ def list_product_categories():
_, _, denied = _ensure_product_category_access()
if denied:
return denied
result, error_response, status = _proxy_backend_java('GET', '/api/admin/product-categories')
keyword = (request.args.get('keyword') or '').strip()
query = f'?keyword={quote(keyword)}' if keyword else ''
result, error_response, status = _proxy_backend_java('GET', f'/api/admin/product-categories{query}')
if error_response is not None:
return error_response, status
payload = result.get('data') or {}
@@ -1225,6 +1228,76 @@ def list_product_categories():
'success': True,
'tree': [_format_product_category_item(item) for item in (payload.get('tree') or [])],
'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])],
'total': payload.get('total') or 0,
'page': payload.get('page') or 1,
'page_size': payload.get('pageSize') or 0,
'has_more': bool(payload.get('hasMore')),
})
@admin_api.route('/product-categories/children')
@login_required
def list_product_category_children():
_ensure_product_category_schema()
_, _, denied = _ensure_product_category_access()
if denied:
return denied
parent_id_raw = (request.args.get('parent_id') or request.args.get('parentId') or '').strip()
page = max(1, int(request.args.get('page', 1)))
page_size = min(100, max(1, int(request.args.get('page_size', request.args.get('pageSize', 20)))))
params = {
'page': page,
'pageSize': page_size,
}
if parent_id_raw:
params['parentId'] = parent_id_raw
result, error_response, status = _proxy_backend_java(
'GET',
'/api/admin/product-categories/children',
params=params,
)
if error_response is not None:
return error_response, status
payload = result.get('data') or {}
return jsonify({
'success': True,
'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])],
'total': payload.get('total') or 0,
'page': payload.get('page') or page,
'page_size': payload.get('pageSize') or page_size,
'has_more': bool(payload.get('hasMore')),
})
@admin_api.route('/product-categories/search')
@login_required
def search_product_categories():
_ensure_product_category_schema()
_, _, denied = _ensure_product_category_access()
if denied:
return denied
keyword = (request.args.get('keyword') or '').strip()
page = max(1, int(request.args.get('page', 1)))
page_size = min(100, max(1, int(request.args.get('page_size', request.args.get('pageSize', 20)))))
result, error_response, status = _proxy_backend_java(
'GET',
'/api/admin/product-categories/search',
params={
'keyword': keyword,
'page': page,
'pageSize': page_size,
},
)
if error_response is not None:
return error_response, status
payload = result.get('data') or {}
return jsonify({
'success': True,
'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])],
'total': payload.get('total') or 0,
'page': payload.get('page') or page,
'page_size': payload.get('pageSize') or page_size,
'has_more': bool(payload.get('hasMore')),
})
@@ -2415,6 +2488,54 @@ def list_query_asins():
})
@admin_api.route('/query-asins/export')
@login_required
def export_query_asins():
role, current_row, denied = _ensure_backend_menu_access('query-asin')
if denied:
return denied
params = {
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
}
group_id_raw = (request.args.get('group_id') or request.args.get('groupId') or '').strip()
shop_name = (request.args.get('shop_name') or request.args.get('shopName') or '').strip()
asin = (request.args.get('asin') or '').strip()
if group_id_raw:
params['groupId'] = group_id_raw
if shop_name:
params['shopName'] = shop_name
if asin:
params['asin'] = asin
url = f"{backend_java_base_url}/api/admin/query-asins/export"
try:
resp = _get_backend_java_session().get(url, params=params, timeout=60)
except requests.RequestException:
return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
if resp.status_code >= 400:
try:
data = resp.json()
error = data.get('message') or data.get('error') or '导出失败'
except ValueError:
error = '导出失败'
return jsonify({'success': False, 'error': error}), resp.status_code
headers = {}
disposition = resp.headers.get('Content-Disposition')
if disposition:
headers['Content-Disposition'] = disposition
return Response(
resp.content,
status=resp.status_code,
headers=headers,
content_type=resp.headers.get(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
),
)
def _format_query_asin_import_progress(progress):
return {
'status': progress.get('status') or 'pending',

View File

@@ -1,14 +1,48 @@
import os
from pathlib import Path
try:
from dotenv import load_dotenv
except ImportError:
def load_dotenv(*args, **kwargs):
return False
base_url = "https://api.coze.cn/v1"
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
workflow_id = "7608812635877900322"
STITCH_WORKFLOW_ID = "7608813873483300907"
BASE_DIR = Path(__file__).resolve().parent
LOADED_ENV_FILES = []
def _load_env_files():
candidates = (
BASE_DIR / ".env",
BASE_DIR.parent / "app" / ".env",
BASE_DIR.parent / ".env",
)
for env_path in candidates:
if env_path.is_file():
load_dotenv(dotenv_path=env_path, override=False)
LOADED_ENV_FILES.append(str(env_path))
def _get_env(*names, default=None):
for name in names:
value = os.getenv(name)
if value not in (None, ""):
return value, name
return default, "config.default"
_load_env_files()
# MySQL 配置
mysql_host = "8.136.19.173"
mysql_user = "aiimage"
mysql_password = "WTFrb5y6hNLz6hNy" # 请修改为您的数据库密码
mysql_database = "aiimage"
mysql_host, mysql_host_source = _get_env("mysql_host", "MYSQL_HOST", default="47.110.241.161")
mysql_user, mysql_user_source = _get_env("mysql_user", "MYSQL_USER", default="aiimage")
mysql_password, mysql_password_source = _get_env("mysql_password", "MYSQL_PASSWORD", default="WTFrb5y6hNLz6hNy")
mysql_database, mysql_database_source = _get_env("mysql_database", "MYSQL_DATABASE", default="aiimage")
cache_path = "./user_data"
@@ -22,20 +56,20 @@ bucket_path = "nanri-image/"
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
import os
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://121.196.149.225:18080').rstrip('/')
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
backend_java_base_url, backend_java_base_url_source = _get_env(
"java_api_base",
"BACKEND_JAVA_BASE_URL",
default="http://127.0.0.1:18080",
# default="http://47.111.163.154:18080",
)
backend_java_base_url = backend_java_base_url.rstrip("/")
os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId
os.environ["OSS_ACCESS_KEY_SECRET"] = accessKeySecret
os.environ["SECRET_KEY"] = "ddffc7c1d02121d9554d7b080b2511b6"
debug = True
version = "1.0.0"
APP_UPDATE_URL = ""
os.environ['APP_VERSION'] = version
os.environ['APP_UPDATE_URL'] = version
os.environ["APP_VERSION"] = version
os.environ["APP_UPDATE_URL"] = version

View File

@@ -44,6 +44,7 @@ PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5_sip==12.17.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pythonnet==3.0.5
pytz==2026.1.post1
pywebview==6.1

View File

@@ -2563,14 +2563,47 @@
}
function buildQueryAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + queryAsinPageSize;
var filterQuery = buildQueryAsinFilterQuery();
if (filterQuery) query += '&' + filterQuery;
return query;
}
function buildQueryAsinFilterQuery() {
var query = '';
var groupId = (document.getElementById('queryAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('queryAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('queryAsinFilterAsin').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
if (asin) query += '&asin=' + encodeURIComponent(asin);
if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId);
if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName);
if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin);
return query;
}
function exportQueryAsin() {
var query = buildQueryAsinFilterQuery();
var url = '/api/admin/query-asins/export' + (query ? ('?' + query) : '');
fetch(url)
.then(function (response) {
var contentType = response.headers.get('content-type') || '';
if (!response.ok || contentType.indexOf('application/json') >= 0) {
return response.json().then(function (res) {
throw new Error((res && (res.error || res.msg)) || '导出失败');
}).catch(function (err) {
throw err instanceof Error ? err : new Error('导出失败');
});
}
return response.blob().then(function (blob) {
return {
blob: blob,
filename: extractDownloadFilename(response.headers.get('content-disposition'), 'query-asin.xlsx')
};
});
})
.then(function (payload) {
triggerBrowserDownload(payload.blob, payload.filename);
})
.catch(function (err) {
alert((err && err.message) || '导出失败');
});
}
function renderQueryAsinCell(item, country) {
var asinValue = item[country.field] || '';
var infoHtml = asinValue ? '<span>' + asinValue + '</span>' : '<span style="color:#999;">-</span>';
@@ -2883,6 +2916,9 @@
document.getElementById('btnSearchQueryAsin').onclick = function () {
loadQueryAsin(1);
};
document.getElementById('btnExportQueryAsin').onclick = function () {
exportQueryAsin();
};
document.getElementById('queryAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
@@ -2964,6 +3000,11 @@
renderQueryAsinInputs();
var productCategoryItems = [];
var productCategoryNodeMap = {};
var productCategoryPageSize = 20;
var productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false };
var productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false };
var productCategoryKeyword = '';
var editingProductCategoryId = null;
function resetProductCategoryForm() {
@@ -2983,27 +3024,102 @@
var selected = selectedValue == null ? select.value : String(selectedValue || '');
var excluded = excludedId == null ? editingProductCategoryId : excludedId;
var options = ['<option value="">顶级类目</option>'];
productCategoryItems.forEach(function (item) {
productCategoryItems.slice().sort(function (a, b) {
return String(a.path || a.name || '').localeCompare(String(b.path || b.name || ''), 'zh-CN');
}).forEach(function (item) {
if (excluded && String(item.id) === String(excluded)) return;
var prefix = new Array((item.level || 0) + 1).join('  ');
options.push('<option value="' + escapeHtml(item.id) + '">' + prefix + escapeHtml(item.name || '') + '</option>');
});
if (selected && !productCategoryItems.some(function (item) { return String(item.id) === selected; })) {
options.push('<option value="' + escapeHtml(selected) + '">当前父级 #' + escapeHtml(selected) + '</option>');
}
select.innerHTML = options.join('');
select.value = selected;
}
function renderProductCategoryRows(items) {
function normalizeProductCategoryItem(item, parent) {
var id = String(item.id || '');
var existing = productCategoryNodeMap[id] || {};
var level = item.level != null ? item.level : (parent ? (parent.level || 0) + 1 : 0);
var path = item.path || (parent && parent.path ? parent.path + ' / ' + (item.name || '') : (item.name || ''));
var normalized = Object.assign(existing, item, {
id: item.id,
parent_id: item.parent_id == null ? null : item.parent_id,
child_count: item.child_count || 0,
level: level,
path: path,
children: existing.children || [],
page: existing.page || 0,
total: existing.total || 0,
hasMore: existing.hasMore !== false,
loaded: existing.loaded || false,
loading: false,
expanded: existing.expanded || false
});
productCategoryNodeMap[id] = normalized;
productCategoryItems = Object.keys(productCategoryNodeMap).map(function (key) { return productCategoryNodeMap[key]; });
return normalized;
}
function flattenVisibleProductCategories(items, output) {
(items || []).forEach(function (item) {
output.push({ type: 'item', item: item });
if ((item.child_count || 0) > 0 && item.expanded) {
flattenVisibleProductCategories(item.children || [], output);
if (item.hasMore) {
output.push({ type: 'more', parent: item });
}
}
});
return output;
}
function renderProductCategoryRows() {
var tbody = document.getElementById('productCategoryListBody');
if (!tbody) return;
if (!items.length) {
var rows = productCategoryKeyword
? productCategorySearchState.items.map(function (item) { return { type: 'item', item: item, search: true }; })
: flattenVisibleProductCategories(productCategoryRootState.children, []);
if (!productCategoryKeyword && productCategoryRootState.hasMore) {
rows.push({ type: 'more', parent: null });
}
if (productCategoryKeyword && productCategorySearchState.hasMore) {
rows.push({ type: 'searchMore' });
}
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无商品类目</td></tr>';
return;
}
tbody.innerHTML = items.map(function (item) {
tbody.innerHTML = rows.map(function (row) {
if (row.type === 'more') {
var parent = row.parent;
var level = parent ? (parent.level || 0) + 1 : 0;
var indentMore = Math.max(0, level) * 24;
var loading = parent ? parent.loading : productCategoryRootState.loading;
var loadedCount = parent ? (parent.children || []).length : (productCategoryRootState.children || []).length;
var totalCount = parent ? (parent.total || 0) : (productCategoryRootState.total || 0);
return '<tr><td colspan="6"><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indentMore + 'px;"></span>' +
'<button class="btn btn-sm btn-secondary" type="button" data-product-category-load-more="' + escapeHtml(parent ? parent.id : '') + '"' + (loading ? ' disabled' : '') + '>' +
(loading ? '加载中...' : '加载更多') + '</button>' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(loadedCount) + ' / ' + escapeHtml(totalCount) + '</span></div></td></tr>';
}
if (row.type === 'searchMore') {
var searchLoadedCount = (productCategorySearchState.items || []).length;
var searchTotalCount = productCategorySearchState.total || 0;
return '<tr><td colspan="6"><button class="btn btn-sm btn-secondary" type="button" data-product-category-search-more' + (productCategorySearchState.loading ? ' disabled' : '') + '>' +
(productCategorySearchState.loading ? '加载中...' : '加载更多搜索结果') + '</button> ' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(searchLoadedCount) + ' / ' + escapeHtml(searchTotalCount) + '</span></td></tr>';
}
var item = row.item;
var indent = Math.max(0, item.level || 0) * 24;
var mark = item.child_count > 0 ? '+' : '-';
var hasChildren = (item.child_count || 0) > 0;
var expanded = !!item.expanded;
var mark = hasChildren ? (expanded ? '-' : '+') : '';
return '<tr>' +
'<td><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indent + 'px;"></span><span class="tree-node-mark">' + mark + '</span><strong>' + escapeHtml(item.name || '') + '</strong></div></td>' +
'<td><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indent + 'px;"></span>' +
'<button type="button" class="tree-node-mark' + (hasChildren ? '' : ' is-leaf') + '" data-product-category-toggle="' + escapeHtml(item.id) + '"' + (hasChildren ? '' : ' disabled') + '>' + mark + '</button>' +
'<strong>' + escapeHtml(item.name || '') + '</strong></div></td>' +
'<td><div>' + escapeHtml(item.path || item.name || '') + '</div><div class="category-path">' + escapeHtml(item.category_key || '') + '</div></td>' +
'<td>' + escapeHtml(item.sort_order || 0) + '</td>' +
'<td><span class="category-tag">' + (item.is_builtin ? '内置' : '自定义') + '</span></td>' +
@@ -3012,6 +3128,24 @@
'<button class="btn btn-sm btn-danger" data-product-category-delete="' + escapeHtml(item.id) + '" data-product-category-name="' + escapeHtml(item.name || '') + '"' + (item.child_count > 0 ? ' disabled' : '') + '>删除</button></td>' +
'</tr>';
}).join('');
document.querySelectorAll('[data-product-category-toggle]').forEach(function (btn) {
btn.onclick = function () {
var id = String(btn.dataset.productCategoryToggle || '');
if (!id || btn.disabled) return;
toggleProductCategoryNode(id);
};
});
document.querySelectorAll('[data-product-category-load-more]').forEach(function (btn) {
btn.onclick = function () {
var parentId = (btn.dataset.productCategoryLoadMore || '').trim();
loadProductCategoryChildren(parentId || null, true);
};
});
document.querySelectorAll('[data-product-category-search-more]').forEach(function (btn) {
btn.onclick = function () {
loadProductCategorySearch(true);
};
});
document.querySelectorAll('[data-product-category-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = productCategoryItems.find(function (row) { return String(row.id) === String(btn.dataset.productCategoryEdit); });
@@ -3047,28 +3181,136 @@
}
function loadProductCategories() {
fetch('/api/admin/product-categories')
if (productCategoryKeyword) {
productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false };
loadProductCategorySearch(false);
return;
}
productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false };
productCategoryNodeMap = {};
productCategoryItems = [];
renderProductCategoryParentOptions();
loadProductCategoryChildren(null, false);
}
function loadProductCategoryChildren(parentId, append) {
var parent = parentId ? productCategoryNodeMap[String(parentId)] : null;
var state = parent || productCategoryRootState;
if (state.loading) return;
state.loading = true;
var failed = false;
renderProductCategoryRows();
var nextPage = append ? (state.page || 0) + 1 : 1;
var query = '?page=' + nextPage + '&page_size=' + productCategoryPageSize;
if (parentId) query += '&parent_id=' + encodeURIComponent(parentId);
fetch('/api/admin/product-categories/children' + query)
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('productCategoryListBody');
if (!res.success) {
failed = true;
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
productCategoryItems = res.items || [];
var children = (res.items || []).map(function (item) {
return normalizeProductCategoryItem(item, parent);
});
if (!append) {
state.children = [];
}
state.children = state.children.concat(children);
state.page = res.page || nextPage;
state.total = res.total || 0;
state.hasMore = !!res.has_more;
state.loaded = true;
renderProductCategoryParentOptions();
renderProductCategoryRows(productCategoryItems);
renderProductCategoryRows();
})
.catch(function () {
failed = true;
var tbody = document.getElementById('productCategoryListBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.finally(function () {
state.loading = false;
if (!failed) renderProductCategoryRows();
});
}
function loadProductCategorySearch(append) {
if (productCategorySearchState.loading) return;
productCategorySearchState.loading = true;
var failed = false;
renderProductCategoryRows();
var nextPage = append ? (productCategorySearchState.page || 0) + 1 : 1;
fetch('/api/admin/product-categories/search?keyword=' + encodeURIComponent(productCategoryKeyword) +
'&page=' + nextPage + '&page_size=' + productCategoryPageSize)
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('productCategoryListBody');
if (!res.success) {
failed = true;
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var items = (res.items || []).map(function (item) {
var normalized = normalizeProductCategoryItem(item, null);
normalized.expanded = false;
return normalized;
});
productCategorySearchState.items = append
? productCategorySearchState.items.concat(items)
: items;
productCategorySearchState.page = res.page || nextPage;
productCategorySearchState.total = res.total || 0;
productCategorySearchState.hasMore = !!res.has_more;
renderProductCategoryParentOptions();
renderProductCategoryRows();
})
.catch(function () {
failed = true;
var tbody = document.getElementById('productCategoryListBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.finally(function () {
productCategorySearchState.loading = false;
if (!failed) renderProductCategoryRows();
});
}
function toggleProductCategoryNode(id) {
var item = productCategoryNodeMap[String(id)];
if (!item) return;
item.expanded = !item.expanded;
if (item.expanded && !item.loaded) {
loadProductCategoryChildren(id, false);
return;
}
renderProductCategoryRows();
}
document.getElementById('btnCancelProductCategoryEdit').onclick = function () {
resetProductCategoryForm();
};
document.getElementById('btnSearchProductCategory').onclick = function () {
productCategoryKeyword = (document.getElementById('productCategoryKeyword').value || '').trim();
loadProductCategories();
};
document.getElementById('btnClearProductCategorySearch').onclick = function () {
productCategoryKeyword = '';
document.getElementById('productCategoryKeyword').value = '';
loadProductCategories();
};
document.getElementById('productCategoryKeyword').onkeydown = function (event) {
if (event.key === 'Enter') {
event.preventDefault();
document.getElementById('btnSearchProductCategory').click();
}
};
document.getElementById('btnCreateProductCategory').onclick = function () {
var msgEl = document.getElementById('msgProductCategory');
var name = (document.getElementById('productCategoryName').value || '').trim();

View File

@@ -10,16 +10,32 @@ try:
from config import mysql_user as config_mysql_user
from config import mysql_password as config_mysql_password
from config import mysql_database as config_mysql_database
from config import mysql_host_source as config_mysql_host_source
from config import mysql_user_source as config_mysql_user_source
from config import mysql_database_source as config_mysql_database_source
except ImportError:
config_mysql_host = 'localhost'
config_mysql_user = 'root'
config_mysql_password = ''
config_mysql_database = 'maixiang_ai'
config_mysql_host_source = 'fallback.default'
config_mysql_user_source = 'fallback.default'
config_mysql_database_source = 'fallback.default'
mysql_host = os.environ.get('MYSQL_HOST', config_mysql_host)
mysql_user = os.environ.get('MYSQL_USER', config_mysql_user)
mysql_password = os.environ.get('MYSQL_PASSWORD', config_mysql_password)
mysql_database = os.environ.get('MYSQL_DATABASE', config_mysql_database)
mysql_host = config_mysql_host
mysql_user = config_mysql_user
mysql_password = config_mysql_password
mysql_database = config_mysql_database
mysql_host_source = config_mysql_host_source
mysql_user_source = config_mysql_user_source
mysql_database_source = config_mysql_database_source
def describe_db_target():
return (
f"{mysql_user}@{mysql_host}/{mysql_database} "
f"(host={mysql_host_source}, user={mysql_user_source}, database={mysql_database_source})"
)
def get_db():

View File

@@ -371,6 +371,7 @@
.tree-node-mark {
width: 18px;
height: 18px;
border: 0;
border-radius: 4px;
background: #eef1ff;
color: #667eea;
@@ -379,6 +380,14 @@
justify-content: center;
font-size: 12px;
font-weight: 700;
cursor: pointer;
padding: 0;
line-height: 1;
}
.tree-node-mark.is-leaf {
background: transparent;
cursor: default;
}
.category-path {
@@ -1317,6 +1326,7 @@
<input type="text" id="queryAsinFilterAsin" placeholder="请输入 ASIN">
</div>
<button class="btn" id="btnSearchQueryAsin">查询</button>
<button class="btn btn-secondary" id="btnExportQueryAsin" type="button">导出 XLSX</button>
</div>
<table>
<thead>
@@ -1365,7 +1375,16 @@
<p class="msg" id="msgProductCategory"></p>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;font-size:15px;">类目树</h3>
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px;">
<h3 style="font-size:15px;">类目树</h3>
<div class="form-row" style="gap:8px;margin:0;flex:0 1 420px;">
<div class="form-group" style="min-width:220px;flex:1;margin:0;">
<input type="text" id="productCategoryKeyword" placeholder="搜索类目名称、编码或备注">
</div>
<button class="btn btn-sm" id="btnSearchProductCategory" type="button">搜索</button>
<button class="btn btn-sm btn-secondary" id="btnClearProductCategorySearch" type="button">清空</button>
</div>
</div>
<table>
<thead>
<tr>