初始提交:完成第一周本地环境搭建与项目运行

sh_branch
严裕丹 5 months ago
parent 76918f2c7f
commit f496e4f042

@ -1,7 +1,7 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)

@ -1,4 +1,4 @@
from haystack import indexes
from blog.models import Article

@ -13,7 +13,8 @@ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from django.db.models import Q
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
@ -283,21 +284,66 @@ class LinkListView(ListView):
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
#class EsSearchView(SearchView):
# def get_context(self):
# paginator, page = self.build_page()
# context = {
# "query": self.query,
# "form": self.form,
# "page": page,
# "paginator": paginator,
# "suggestion": None,
# }
# if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
# context["suggestion"] = self.results.query.get_spelling_suggestion()
# context.update(self.extra_context())
#
# return context
# 移除原来的 EsSearchView 类,替换为原生搜索视图
def search_view(request):
"""
Django原生搜索视图
替换原来的haystack搜索功能
"""
query = request.GET.get('q', '').strip()
results = Article.objects.none()
results_count = 0
if query:
# 多字段搜索:标题、内容、摘要
search_conditions = Q(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(summary__icontains=query)
)
results = Article.objects.filter(
search_conditions,
status='p', # 只搜索已发布的文章
type='a' # 只搜索文章类型
).distinct().order_by('-created_time')
results_count = results.count()
# 分页设置
paginator = Paginator(results, settings.PAGINATE_BY)
page_number = request.GET.get('page')
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
context = {
'results': page_obj,
'query': query,
'results_count': results_count,
'page_obj': page_obj,
'page_type': '搜索结果',
}
return render(request, 'search/search.html', context)
@csrf_exempt

@ -2,6 +2,7 @@ from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from blog.models import Article

@ -1 +1,3 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
import pymysql
pymysql.install_as_MySQLdb()

@ -1,183 +0,0 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -53,7 +53,7 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
@ -109,14 +109,14 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
'NAME': 'djangoblog',
'USER': 'root', # 通常是 root
'PASSWORD': '050322yyd*', # MySQL安装时设置的密码
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS':{
'charset': 'utf8mb4',
}
}}
# Password validation
@ -160,14 +160,7 @@ USE_TZ = False
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
@ -326,11 +319,7 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'

@ -19,11 +19,10 @@ from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from blog.views import EsSearchView
from blog.views import search_view
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
@ -41,7 +40,8 @@ handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
#path('i18n/', include('django.conf.urls.i18n')),
path('search/', search_view, name='search'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
@ -54,8 +54,8 @@ urlpatterns += i18n_patterns(
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
#path('search/', search_view, name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

@ -14,14 +14,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
from haystack.inputs import Clean, Exact, PythonData, Raw
from haystack.models import SearchResult
from haystack.utils import get_identifier, get_model_ct
from haystack.utils import log as logging
from haystack.utils.app_loading import haystack_get_model
from jieba.analyse import ChineseAnalyzer
from whoosh import index
from whoosh.analysis import StemmingAnalyzer
@ -101,13 +94,12 @@ class WhooshSearchBackend(BaseSearchBackend):
"You must specify a 'PATH' in your settings for connection '%s'." %
connection_alias)
self.log = logging.getLogger('haystack')
def setup(self):
"""
Defers loading until needed.
"""
from haystack import connections
new_index = False
# Make sure the index is there.
@ -150,7 +142,7 @@ class WhooshSearchBackend(BaseSearchBackend):
DJANGO_CT: WHOOSH_ID(stored=True),
DJANGO_ID: WHOOSH_ID(stored=True),
}
# Grab the number of keys that are hard-coded into Haystack.
# We'll use this to (possibly) fail slightly more gracefully later.
initial_key_count = len(schema_fields)
content_field_name = ''
@ -438,9 +430,7 @@ class WhooshSearchBackend(BaseSearchBackend):
narrowed_results = None
self.index = self.index.refresh()
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
@ -582,10 +572,7 @@ class WhooshSearchBackend(BaseSearchBackend):
narrowed_results = None
self.index = self.index.refresh()
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models:
@ -682,7 +669,7 @@ class WhooshSearchBackend(BaseSearchBackend):
query_string='',
spelling_query=None,
result_class=None):
from haystack import connections
results = []
# It's important to grab the hits first before slicing. Otherwise, this
@ -701,7 +688,7 @@ class WhooshSearchBackend(BaseSearchBackend):
score = raw_page.score(doc_offset) or 0
app_label, model_name = raw_result[DJANGO_CT].split('.')
additional_fields = {}
model = haystack_get_model(app_label, model_name)
if model and model in indexed_models:
for key, value in raw_result.items():
@ -903,7 +890,7 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
from haystack import connections
query_frag = ''
is_datetime = False

Binary file not shown.

@ -1,27 +1,43 @@
from haystack.query import SearchQuerySet
import logging
from django.http import JsonResponse
from django.db.models import Q
from blog.models import Article, Category
logger = logging.getLogger(__name__)
class BlogApi:
def __init__(self):
self.searchqueryset = SearchQuerySet()
self.searchqueryset.auto_query('')
self.__max_takecount__ = 8
self.searchqueryset = Article.objects.all()
def search_articles(self, query):
sqs = self.searchqueryset.auto_query(query)
sqs = sqs.load_all()
return sqs[:self.__max_takecount__]
if query:
# 使用 Q 对象进行多字段搜索
results = Article.objects.filter(
Q(title__icontains=query) |
Q(body__icontains=query) |
Q(category__name__icontains=query) |
Q(tags__name__icontains=query)
).filter(status='p') # 只搜索已发布的文章
return results.distinct()[:self.__max_takecount__]
return Article.objects.none()
def get_category_lists(self):
return Category.objects.all()
def get_category_articles(self, categoryname):
articles = Article.objects.filter(category__name=categoryname)
articles = Article.objects.filter(category__name=categoryname,status='p')
if articles:
return articles[:self.__max_takecount__]
return None
def get_recent_articles(self):
return Article.objects.all()[:self.__max_takecount__]
return Article.objects.filter(
status='p'
).order_by('-pub_time')[:self.__max_takecount__]
def get_popular_articles(self):
"""获取热门文章(按浏览量排序)"""
return Article.objects.filter(
status='p'
).order_by('-views')[:self.__max_takecount__]

@ -1,66 +1,233 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load static %}
{% block header %}
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<title>{% if query %}搜索 "{{ query }}" - {% endif %}{{ SITE_NAME }}</title>
<meta name="description" content="搜索{{ SITE_NAME }}的文章内容"/>
<meta name="keywords" content="搜索,{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:title" content="搜索 - {{ SITE_NAME }}"/>
<meta property="og:description" content="搜索{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}/search/"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<!-- 搜索框 -->
<div class="search-box-container" style="margin-bottom: 30px;">
<form method="get" action="{% url 'search' %}" class="search-form">
<div class="search-input-group">
<input type="text" name="q" class="search-input"
placeholder="输入关键词搜索文章..."
value="{{ query|default:'' }}"
aria-label="搜索">
<button type="submit" class="search-button">
<i class="fas fa-search"></i> 搜索
</button>
</div>
</form>
</div>
{% if query %}
<header class="archive-header">
{% if suggestion %}
<h2 class="archive-title">
已显示<span style="color: red"> “{{ suggestion }}” </span>的搜索结果。&nbsp;&nbsp;
仍然搜索:<a style="text-transform: none;" href="/search/?q={{ query }}&is_suggest=no">{{ query }}</a> <br>
</h2>
{% else %}
<h2 class="archive-title">
搜索:<span style="color: red">{{ query }} </span> &nbsp;&nbsp;
</h2>
{% endif %}
<h2 class="archive-title">
搜索:<span style="color: red">"{{ query }}"</span>
{% if results_count %}
<span style="font-size: 0.8em; color: #666; margin-left: 15px;">
找到 {{ results_count }} 条结果
</span>
{% endif %}
</h2>
</header><!-- .archive-header -->
{% endif %}
{% if query and page.object_list %}
{% for article in page.object_list %}
{% load_article_detail article.object True user %}
{% if query and results %}
<!-- 显示搜索结果 -->
{% for article in results %}
{% load_article_detail article True user %}
{% endfor %}
{% if page.has_previous or page.has_next %}
<!-- 分页导航 -->
{% if page_obj.has_previous or page_obj.has_next %}
<nav id="nav-below" class="navigation" role="navigation">
<h3 class="assistive-text">文章导航</h3>
{% if page.has_previous %}
<div class="nav-previous"><a
href="?q={{ query }}&amp;page={{ page.previous_page_number }}"><span
class="meta-nav">&larr;</span> 早期文章</a></div>
{% if page_obj.has_previous %}
<div class="nav-previous">
<a href="?q={{ query }}&amp;page={{ page_obj.previous_page_number }}">
<span class="meta-nav">&larr;</span> 上一页
</a>
</div>
{% endif %}
{% if page.has_next %}
<div class="nav-next"><a href="?q={{ query }}&amp;page={{ page.next_page_number }}">较新文章
<span
class="meta-nav">→</span></a>
<span class="page-numbers" style="margin: 0 15px;">
第 {{ page_obj.number }} 页 / 共 {{ page_obj.paginator.num_pages }} 页
</span>
{% if page_obj.has_next %}
<div class="nav-next">
<a href="?q={{ query }}&amp;page={{ page_obj.next_page_number }}">
下一页 <span class="meta-nav">&rarr;</span>
</a>
</div>
{% endif %}
</nav><!-- .navigation -->
{% endif %}
{% else %}
<header class="archive-header">
<h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1>
</header><!-- .archive-header -->
{% else %}
<!-- 无结果时的显示 -->
{% if query %}
<header class="archive-header">
<h1 class="archive-title">
哎呀,关键字:<span style="color: red">{{ query }}</span>没有找到结果,要不换个词再试试?
</h1>
<div class="search-suggestions" style="margin-top: 20px;">
<p>搜索建议:</p>
<div class="suggestion-tags">
<a href="?q=Django" class="tag">Django</a>
<a href="?q=Python" class="tag">Python</a>
<a href="?q=Web开发" class="tag">Web开发</a>
<a href="?q=数据库" class="tag">数据库</a>
</div>
</div>
</header>
{% else %}
<header class="archive-header">
<h1 class="archive-title">欢迎使用搜索功能</h1>
<p>请在搜索框中输入关键词来查找您感兴趣的文章。</p>
</header>
{% endif %}
{% endif %}
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar request.user 'i' %}
{% endblock %}
{% block extra_css %}
<style>
.search-box-container {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.search-form {
margin: 0;
}
.search-input-group {
display: flex;
max-width: 600px;
margin: 0 auto;
}
.search-input {
flex: 1;
padding: 12px 15px;
border: 2px solid #007bff;
border-right: none;
border-radius: 4px 0 0 4px;
font-size: 16px;
outline: none;
}
.search-button {
padding: 12px 20px;
background: #007bff;
color: white;
border: 2px solid #007bff;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 16px;
transition: background 0.3s ease;
}
.search-button:hover {
background: #0056b3;
border-color: #0056b3;
}
.search-suggestions {
text-align: center;
}
.suggestion-tags {
margin-top: 10px;
}
.tag {
display: inline-block;
margin: 5px;
padding: 8px 15px;
background: #e9ecef;
color: #495057;
text-decoration: none;
border-radius: 20px;
transition: all 0.3s ease;
}
.tag:hover {
background: #007bff;
color: white;
text-decoration: none;
}
.navigation {
display: flex;
justify-content: center;
align-items: center;
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
}
.nav-previous, .nav-next {
margin: 0 10px;
}
.nav-previous a, .nav-next a {
padding: 10px 20px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
text-decoration: none;
color: #495057;
transition: all 0.3s ease;
}
.nav-previous a:hover, .nav-next a:hover {
background: #007bff;
color: white;
border-color: #007bff;
}
.page-numbers {
color: #6c757d;
font-size: 14px;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 搜索框获得焦点时的事件
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('focus', function() {
this.parentElement.style.boxShadow = '0 0 10px rgba(0, 123, 255, 0.3)';
});
searchInput.addEventListener('blur', function() {
this.parentElement.style.boxShadow = 'none';
});
}
});
</script>
{% endblock %}

@ -25,6 +25,120 @@
<a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a>
</li>
<li class="menu-item menu-item-search">
<div class="nav-search-form">
<form method="get" action="{% url 'search' %}">
<div class="search-container">
<input type="text" name="q" class="search-input"
placeholder="{% trans 'Search articles...' %}"
value="{{ request.GET.q|default:'' }}"
aria-label="{% trans 'Search' %}">
<button type="submit" class="search-button" title="{% trans 'Search' %}">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
</li>
</ul>
</div>
</nav><!-- #site-navigation -->
</nav><!-- #site-navigation -->
<style>
.menu-item-search {
margin-left: auto;
padding: 0 10px;
}
.nav-search-form {
display: flex;
align-items: center;
height: 100%;
}
.search-container {
display: flex;
align-items: center;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 20px;
overflow: hidden;
transition: all 0.3s ease;
}
.search-input {
border: none;
background: transparent;
padding: 8px 12px;
outline: none;
width: 150px;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #888;
}
.search-button {
border: none;
background: transparent;
padding: 8px 10px 8px 5px;
cursor: pointer;
color: #666;
transition: color 0.3s ease;
}
.search-button:hover {
color: #0073aa;
}
.search-container:focus-within {
border-color: #0073aa;
box-shadow: 0 0 5px rgba(0, 115, 170, 0.3);
background: #fff;
}
/* 响应式设计 - 移动设备适配 */
@media (max-width: 768px) {
.menu-item-search {
order: 100; /* 确保在移动设备上显示在最后 */
width: 100%;
padding: 10px 0;
margin-left: 0;
border-top: 1px solid #eee;
}
.search-container {
width: 100%;
max-width: 300px;
margin: 0 auto;
}
.search-input {
width: 100%;
flex: 1;
}
.nav-menu {
flex-wrap: wrap;
}
}
/* 桌面端的大屏幕适配 */
@media (min-width: 1024px) {
.search-input {
width: 180px;
}
}
/* 小屏幕手机适配 */
@media (max-width: 480px) {
.search-input {
font-size: 16px; /* 防止iOS缩放 */
}
.search-container {
border-radius: 25px;
}
}
</style>
Loading…
Cancel
Save