第九章Admin后台系统

摘要:
第9章管理后台系统django。contrib公司。admin',默认情况下在admin后台系统中启用。在创建帐户密码之前,请确保项目已执行数据迁移。在数据库中创建相应的数据表。中文显示背景将中间件放置在settings.py‘django中第三个中间件的位置。中间件。地方的LocaleMiddleware”。有两种方法可以将模型从django注册到后台。贡献进口

第九章 Admin后台系统

默认启用的Admin后台系统
'django.contrib.admin',
创建账户密码之前,确保项目已执行数据迁移,在数据库中创建相应的数据表

中文显示后台

把该中间件位于settings.py中的第三个中间件的位置
'django.middleware.locale.LocaleMiddleware',

注册模型到后台的两种方法

from django.contrib import admin
from .models import *


# 方法一:
# 将模型直接注册到admin后台
# admin.site.register(PersonInfo)

# 方法二:
# 自定义PersonInfoAdmin类并继承ModelAdmin
# 注册方法一,使用装饰器将PersonInfoAdmin和Product绑定
@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin): 
    # 设置显示的字段
    list_display = ['name', 'age'] # list_display显示人员信息
# 注册方法二
# admin.site.register(PersonInfo, PersonInfoAdmin)
后台默认会有增加xxx(设置verbose_name即为该字段,否则为数据表名)

源码分析 ModelAdmin

位置:django/contrib/admin/options.py中
ModelAdmin继承自BaseModelAdmin,而父类BaseModelAdmin的元类为MediaDefiningClass,因此Admin系统的属性和方法来及ModelAdmin和BaseModelAdmin
列出日常开发中常用的属性和方法(关于admin后台该展示的字段和方法)

fields 在数据新增或修改的页面设置可编辑的字段
exclude 在数据新增或修改的页面设置不可编辑的字段
fieldsets 改变新增或修改页面的网页布局

# 在数据列表页设置日期选择器
    date_hierarchy = 'recordTime'
# 设置可搜索的字段
    search_fields = ['job', 'title']
 # 为数据列表页的字段id和job设置编辑状态
    list_editable = ['job', 'title']

实例models.py

from django.db import models

# Create your models here.
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    def __str__(self):
        return self.name
    class Meta:
        verbose_name = '人员信息'

class Vocation(models.Model):
    JOB = (
        ('软件开发', '软件开发'),
        ('软件测试', '软件测试'),
        ('需求分析', '需求分析'),
        ('项目管理', '项目管理'),
    )
    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20, choices=JOB)
    title = models.CharField(max_length=20)
    payment = models.IntegerField(null=True, blank=True)
    person = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
    recordTime = models.DateField(auto_now=True, null=True, blank=True)

    def __str__(self):
        return str(self.id)
    class Meta:
        verbose_name = '职业信息'

admin.py

from django.contrib import admin
from .models import *


# 方法一:
# 将模型直接注册到admin后台
# admin.site.register(PersonInfo)

# 方法二:
# 自定义PersonInfoAdmin类并继承ModelAdmin
# 注册方法一,使用装饰器将PersonInfoAdmin和Product绑定
@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


# 注册方法二
# admin.site.register(PersonInfo, PersonInfoAdmin)


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据新增或修改的页面设置可编辑的字段
    # fields = ['job','title','payment','person']

    # 在数据新增或修改的页面设置不可编辑的字段
    # exclude = []

    # 改变新增或修改页面的网页布局(admin后台的新增或修改,修改直接点解id号就可以)
    fieldsets = (
        ('职业信息', {
            'fields': ('job', 'title', 'payment')
        }),  
        ('人员信息', { # 第一项是显示信息,第二项是隐藏信息
            # 设置隐藏与显示
            'classes': ('collapse',),
            'fields': ('person',),
        }),
    )

    # 将下拉框改为单选按钮
    # admin.HORIZONTAL是水平排列
    # admin.VERTICAL是垂直排列
    radio_fields = {'person': admin.VERTICAL}

    # 在数据新增或修改的页面设置可读的字段,不可编辑
    # readonly_fields = ['job',]

    # 设置排序方式,['id']为升序,降序为['-id']
    ordering = ['id']

    # 设置数据列表页的每列数据是否可排序显示
    sortable_by = ['job', 'title']

    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'payment', 'person']

    # 为数据列表页的字段id和job设置路由地址,该路由地址可进入数据修改页
    # list_display_links = ['id', 'job']

    # 设置过滤器,如有外键,应使用双下画线连接两个模型的字段
    list_filter = ['job', 'title', 'person__name']

    # 在数据列表页设置每一页显示的数据量
    list_per_page = 100

    # 在数据列表页设置每一页显示最大上限的数据量
    list_max_show_all = 200

    # 为数据列表页的字段id和job设置编辑状态
    list_editable = ['job', 'title']

    # 设置可搜索的字段
    search_fields = ['job', 'title']

    # 在数据列表页设置日期选择器
    date_hierarchy = 'recordTime'

    # 在数据修改页添加“另存为”功能
    save_as = True

    # 设置“动作”栏的位置
    actions_on_top = False
    actions_on_bottom = True

admin首页设置

修改title和header(登录界面),在admin.py中添加即可

admin.site.site_title = 'MyDjango后台管理' # 这个是显示在url栏目的上方的,像是网页的title
admin.site.site_header = 'MyDjango' # 只有这个显示在登录界面

若想将app应用的英文改为中文(后台管理页面的),也就是修改app在后台的名称,在admin所在文件夹的__init__.py文件夹下设置即可
当然下面方法可以复用到任何一个app下,但主项目文件夹下不可以
只需要复写default_app_config,他的构成是前半部分是应用名,后半部分统一为IndexConfig。所以只需根据apps.py中的name属性的值更改。
还需更改verbose_name,这个为真正的应用名

from django.apps import AppConfig
import os
# 修改App在Admin后台显示的名称
# default_app_config的值的前半部分是应用名,后半部分统一为IndexConfig
default_app_config = 'index.IndexConfig'

# 获取当前App的命名
def get_current_app_name(_file):
    return os.path.split(os.path.dirname(_file))[-1]

# 重写类IndexConfig
class IndexConfig(AppConfig):
    name = get_current_app_name(__file__)
    verbose_name = 'index应用' # 改应用名字在这里修改

admin后台系统首页的设置

包括:

项目应用的显示名称 :

init.py中

模型的显示名称:

models.py中的 verbose_name和verbose_name_plural

网页标题:

admin.py中设置admin.site.site_title 和
admin.site.site_head属性
如果项目中有多个应用,只需在一个应用下的admin.py设置即可(网页标题)

实现Admin的二次开发

重写ModelAdmin的方法,以下介绍的函数均是

get_readonly_fields

get_readonly_fields函数是由BaseModelAdmin定义的,他获取readonly_fields的属性值,从而将模型字段设为只读属性,通过重写此函数可以自定义模型字段的只读属性,比如根据不同的用户角色来设置模型字段的只读属性。

# 重写get_readonly_fields函数
# 设置超级管理员和普通用户的权限
def get_readonly_fields(self, request, obj=None):
    # request是当前的请求对象,obj是模型对象默认值为None,代表当前网页为数据新增页,否则为数据修改页
    if request.user.is_superuser: # 判断用户是否为超级管理员
        self.readonly_fields = [] # 只读字段为空
    else:
        self.readonly_fields = ['payment'] # readonly_fields设置为只读字段
    return self.readonly_fields

设置字段样式

数据列表页显示的模型字段是由list_display设置的,每个字段的数据都来自数据表,并且数据以固定的字体格式显示在网页上,若要对某些字段的数据进行特殊处理,如设置数据的字体颜色,。

# 自定义函数,设置字体颜色
# 但他不是模型字段,可在admin.py中用list_display.append('colored_name')加入模型的数据列表页
def colored_name(self):
    if 'Lucy' in self.person.name: # 获取PersonInfo数据表的字段name,缘由person是外键
        color_code = 'red'
    else:
        color_code = 'blue'
    return format_html( # format_html内置的html转义
        '<span style="color: {};">{}</span>', # 行内样式
        color_code,
        self.person.name,
    )

# 设置Admin的标题
# short_description属性使该colored_name()函数以字段的形式显示在模型Vocation的数据列表页
colored_name.short_description = '带颜色的姓名' # 带颜色的姓名是标题,

# 捕获的Django内置函数和属性,即不需要导包的
# short_description使该模型类中的函数(colored_name())以字段的形式显示在模型(Vocation)的数据列表页
# format_html()内置的html转义,使用方法类似于format的f用法;f'name:{变量名}',这里变成了逗号分隔,这里是函数所以这样传参

函数get_queryset()

用于查询模型的数据信息,然后再Admin的数据列表页展示。默认情况下,该函数执行全表数据查询,若要改变数据的查询方式,则可重新定义该函数,比如根据不同的用户角色执行不同的数据查询。

# 根据当前用户名设置数据访问权限
def get_queryset(self, request):
    qs = super().get_queryset(request)  # 调用父类的该方法获取查询对象,用该查询对象查询模型Vocation的全部数据,否则返回模型字段id小于2的数据
    if request.user.is_superuser:
        return qs
    else:
        return qs.filter(id__lt=2)

formfield_for_foreignkey

新增或修改数据时,设置外键可选值,就是该字段为下拉框(外键通常都是一对多,一个person,对应着各种名字)
formfield_for_foreignkey只适用于模型一对一或一对多;多对多为formfield_for_manytomany

def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == 'person': # db_field模型的字段对象,因为一个模型可以定义多个外键字段,所以需要对特定的字段做判断
        if not request.user.is_superuser: # request为当前用户的请求对象
            v = Vocation.objects.filter(id__lt=2)
            # 将该模型字段id小于2的数据作为模型PersonInfo的查询条件参数
            kwargs['queryset'] = PersonInfo.objects.filter(id__in=v) # 这其实就是外键关联查询吧
            # 上述就是将整个查询得到的对象打包成字典对象发给父对象的方法
      # 传递形参给父类的方法,由父类的函数从形参kwargs中获取参数queryset,从而实现数据的过滤
    return super().formfield_for_foreignkey(db_field, request, **kwargs) # 不用父类试试,报错RecursionError: maximum recursion depth exceeded

formfield_for_choice_field

过滤下拉框数据

    # 内置函数formfield_for_choice_field虽然能新增下拉框的选项内容,
    # 但在保存数据的过程中,Django会提示新增的选项内容是无效的,因此该函数常用于过滤已存在的选项。
    # db_field.choices获取模型字段的属性choices的值 ;这里就是重新定义内置函数formfield_for_choice_field
    def formfield_for_choice_field(self, db_field, request, **kwargs):
        if db_field.name == 'job': # 过滤下拉框的字段,默认为全部已添加的字段,再models.py中以定义
            # 减少字段job可选的选项 
            kwargs['choices'] = (('软件开发', '软件开发'),
                                 ('软件测试', '软件测试'),)
        return super().formfield_for_choice_field(db_field, request, **kwargs)

    # 如果要对字段的下拉框’新增‘内容,可以重新定义formfield_for_dbfield函数
    # Django首先执行formfield_for_dbfield,然后再执行formfield_for_choice_field
    # 如果我们都重写了formfield_for_dbfield和formfield_for_choice_field
    # 最后下拉框的选项以formfield_for_choice_field为准
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        if db_field.name == 'job':
            # 必须判断选项内容是否已存在db_field.choices,如果不判断,则会重复新增选项
            if ('网页设计', '网页设计') not in db_field.choices:
                db_field.choices += (('网页设计', '网页设计'),)
        return super().formfield_for_dbfield(db_field, request, **kwargs)

save_model()

在后台页面单击添加(修改)XXX信息,单击保存按钮所触发的功能,该函数主要对输入的数据进行入库或修改处理。
若想加入的是一些特殊功能,可进行复写,如:添加日志数据,把你需要修改的字段写入文件中

    def save_model(self, request, obj, form, change):
        if change: # change判断当前请求是来自数据修改页还是来自数据新增页,数据修改页则为true
            # 获取当前用户名
            user = request.user.username
            # 使用模型获取数据,’pk代表具有主键属性的字段‘
            job = self.model.objects.get(pk=obj.pk).job
            # 使用表单获取数据
            person = form.cleaned_data['person'].name
            # 写入日志文件
            f = open('d://log.txt', 'a')
            f.write(person + '职位:' + job + ',被' + user + '修改' + '\r\n')
            f.close()
        else:
            pass
        # 使用super在继承父类已有的功能下新增自定义功能
        # 不调用super()方法的话,程序只执行保存日志,但不执行数据入库和修改处理
        super().save_model(request, obj, form, change)

同样的执行数据删除操作也是一样的,Django调用函数delete_model()实现,该函数设有的默认参数为self,request.可以仿照save_model()函数来进行修改。可以实现在你删除数据时候,把删除的字段添加到日志文件中(本地新建个)

数据批量操作

def get_datas(self, request, queryset):
    temp = []
    for d in queryset: # 从已被勾选的数据对象里获取模型字段的数据内容
        t = [d.job, d.title, str(d.payment), d.person.name] # 每行数据以列表t表示
        temp.append(t) # 将列表t加入列表
    f = open('d://data.txt', 'a')
    for t in temp:
        f.write(','.join(t) + '\r\n') # 写入数据\r\n \r\n,这样写就不会连在一起了
        
    f.close()
    # 设置提示信息
    self.message_user(request, '数据导出成功!')
# 设置函数的显示名称
get_datas.short_description = '导出所选数据'
# 添加到“动作”栏
actions = ['get_datas'] # 内置属性actions

自定义Admin模板

源码路径:django/contrib/admin/templates/admin
这里使用继承admin模板,来自定义开发,最好别修改admin初始的模板,缘由是整个django的admin都会被更改,如果你没使用虚拟环境的话。
过程:
在模板文件templates下依次创建文件夹admin和index.
1.文件夹admin代表该文件夹里的模板文件用于Admin后台系统,而且文件夹必须命名为admin
2.文件夹index代表项目应用index,文件夹的命名必须与项目应用的命名一致。文件夹存放模板文件change_form.html,所有在项目应用index中定义的模型都会使用该模板文件生成网页信息。
3.如果将模板文件change_form.html放在admin文件夹下,那么整个Admin后台系统都会使用该模板文件生成网页信息。
重写的change_form.html来自Django内置模板文件admin/change_form.html
该实例是判断用户角色不同,来显示后台对应的页面,当然是在原始admin后台基础上新增了一部分

{% extends "admin/change_form.html" %}
{% load i18n admin_urls static admin_modify %} <!--内置模板admin/change_form.html导入了该模板,所以自定义模板也需要-->
{% block object-tools-items %}
    {# 判断当前用户角色 #}
    {% if request.user.is_superuser %}
        <li>
            {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
            <a href="http://t.zoukankan.com/{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
        </li>
    {# 判断结束符 #}
    {% endif %}
    {% if has_absolute_url %}
        <li><a href="http://t.zoukankan.com/{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>
    {% endif %}
{% endblock %}

自定义Admin后台系统

Admin后台系统为每个网页设置了具体的路由地址(啥意思)
重新定义Admin后台系统,比如常见的Xadmin和Django Suit,这些插件都是在Admin后台系统的基础上进行重新定义的
admin后台系统由类AdminSite实例化创建而成的,换句话说,重新定义类AdminSite即可实现Admin后台系统的自定义开发(源码位置:django/contrib/admin/sites.py)

admin后台系统的注册过程

当运行django时,Admin后台系统会随之运行。Admin的系统注册过程在源码apps.py中定义。
django.contrib.admin 中的 apps.py

综上

要实现Admin后台系统的自定义开发,就需要重新定义类AdminSite和改变Admin的系统注册过程。

简单讲解如何更换Admin后台系统的登录页面

在项目更目录下创建static文件,并加入所需的javascript文件和css文件,然后再把templates文件夹中放置登录页面login.html,最后在MyDjango文件夹创建文件myadmin.py和myapps.py.

实例

login.html

<!DOCTYPE html>
<html>
<head>
    {% load static %}
	<title>MyDjango后台登录</title>
	<link rel="stylesheet" href="http://t.zoukankan.com/{% static "css/reset.css" %}">
	<link rel="stylesheet" href="http://t.zoukankan.com/{% static "css/user.css" %}">
    <script src="http://t.zoukankan.com/{% static "js/jquery.min.js" %}"></script>
    <script src="http://t.zoukankan.com/{% static "js/user.js" %}"></script>
</head>
<body>
<div class="page">
	<div class="loginwarrp">
		<div class="logo">用户登录</div>
        <div class="login_form">
			<form   name="Login" method="post" action="">
                {% csrf_token %}
				<li class="login-item">
					<span>用户名:</span>
					<input type="text" name="username" class="login_input">
                    <span   class="error"></span>
				</li>
				<li class="login-item">
					<span>密 码:</span>
					<input type="password" name="password" class="login_input">
                    <span   class="error"></span>
				</li>
				<li class="login-sub">
					<input type="submit" name="Submit" value="登录">
				</li>				
           </form>
		</div>
	</div>
</div>
<script type="text/javascript">
	window.onload = function() {
		var config = {
			vx : 4,
			vy : 4,
			height : 2,
			width : 2,
			count : 100,
			color : "121, 162, 185",
			stroke : "100, 200, 180",
			dist : 6000,
			e_dist : 20000,
			max_conn : 10
		};
		CanvasParticle(config);
	}
</script>
<script src="http://t.zoukankan.com/{% static "js/canvas-particle.js" %}"></script>
</body>
</html>

myadmin.py
在myadmin.py中定义类MyAdminSite,他继承父类admin.AdminSite并重写方法admin_view()和get_urls(),从而更改admin()后台的登录界面

from django.contrib import admin
from functools import update_wrapper
from django.views.generic import RedirectView
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.http import HttpResponseRedirect
from django.contrib.auth.views import redirect_to_login
from django.urls import include, path, re_path
from django.contrib.contenttypes import views as contenttype_views
class MyAdminSite(admin.AdminSite):
    def admin_view(self, view, cacheable=False):
        def inner(request, *args, **kwargs):
            if not self.has_permission(request):
                if request.path == reverse('admin:logout', current_app=self.name):
                    index_path = reverse('admin:index', current_app=self.name)
                    return HttpResponseRedirect(index_path)
                # 修改注销后重新登录的路由地址
                return redirect_to_login(
                    request.get_full_path(),
                    '/login.html'
                )
            return view(request, *args, **kwargs)
        if not cacheable:
            inner = never_cache(inner)
        if not getattr(view, 'csrf_exempt', False):
            inner = csrf_protect(inner)
        return update_wrapper(inner, view)

    def get_urls(self):
        def wrap(view, cacheable=False):
            def wrapper(*args, **kwargs):
                return self.admin_view(view, cacheable)(*args, **kwargs)
            wrapper.admin_site = self
            return update_wrapper(wrapper, view)
        urlpatterns = [
            path('', wrap(self.index), name='index'),
            # 修改登录界面的路由地址
            path('login/', RedirectView.as_view(url='/login.html')),
            path('logout/', wrap(self.logout), name='logout'),
            path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'),
            path(
                'password_change/done/',
                wrap(self.password_change_done, cacheable=True),
                name='password_change_done',
            ),
            path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
            path(
                'r/<int:content_type_id>/<path:object_id>/',
                wrap(contenttype_views.shortcut),
                name='view_on_site',
            ),
        ]
        valid_app_labels = []
        for model, model_admin in self._registry.items():
            urlpatterns += [
                path('%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
            ]
            if model._meta.app_label not in valid_app_labels:
                valid_app_labels.append(model._meta.app_label)
        if valid_app_labels:
            regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
            urlpatterns += [
                re_path(regex, wrap(self.app_index), name='app_list'),
            ]
        return urlpatterns

myapps.py

from django.contrib.admin.apps import AdminConfig
# 继承父类AdminConfig
# 重新设置属性default_site的值,使它指向MyAdminSite类
class MyAdminConfig(AdminConfig):
    default_site = 'MyDjango.myadmin.MyAdminSite'

在settings.py中配置系统注册类MyAdminConfig.最后在配置静态资源文件static

INSTALLED_APPS = [
    # 注释原有的admin
    # 'django.contrib.admin',
    # 指向myapps的MyAdminConfig
    'MyDjango.myapps.MyAdminConfig',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index'
]

# 配置静态资源文件static
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

笔记来源:Django Web应用开发实战

免责声明:文章转载自《第九章Admin后台系统》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇预编译头文件TCP连接中存在大量TIME_WAIT、CLOSE_WAIT的原因下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

解决win10系统与服务器远程连接明明密码正确却显示身份验证错误问题

升级至win10 最新版本10.0.17134,远程桌面连接Window Server时报错信息如下: 出现身份验证错误,要求的函数不正确,这可能是由于CredSSP加密Oracle修正。 解决方法: 运行 gpedit.msc 本地组策略: 计算机配置>管理模板>系统>凭据分配>加密Oracle修正 选择启用并选择易受攻击。...

基于React Native的58 APP开发实践

React Native在iOS界早就炒的火热了,随着2015年底Android端推出后,一套代码能运行于双平台上,真正拥有了Hybrid框架的所有优势。再加上Native的优秀性能,让越来越多的公司在实际项目中一探究竟。58同城APP发布模块年代久远,一直计划进行重构以适应日益苛刻的用户体验,这个需求与我们在React Native上一探究竟的意愿一碰撞...

01. go-admin的下载与启动

目录 一.介绍 二.新建空文件夹 三.获取后台源码并启动 1.下载编译go代码 2.配置命令到goland IDE ,debug启动 四.获取前端ui源码并启动 1.下载编译go代码 2.启动项目 ** go-admin 基于Gin + Vue + Element UI的前后端分离权限管理系统** 系统初始化极度简单,只需要配置文件...

Linux下安装RabbitMQ

前言 RabbitMQ是一个开源的消息中间件,采用 Erlang 语言进行编写,因此RabbitMQ的安装需要依赖Erlang,现在我们将在 Linux 下进行安装RabbitMQ。 本人环境:CentOS 6.5 64位 安装Erlang 在安装Erlang的时候,有很多种方法,最开始我是想按照官网先下载Erlang安装包,然后再进行安装。但发现下载Er...

Navicat Premium 15 安装包&amp;amp;激活工具及安装教程(亲测可用)

Navicat Premium 15 安装包及激活工具 网盘地址: 链接:https://pan.baidu.com/s/1GU9qgdG1dRCw9Un8H9Ba9A提取码:F1r9 开始安装 下载好后首先安装 Navicat Premium 15 ,此工具安装较为简单,傻瓜式next安装方法,但建议更改安装路径. 开始激活(激活时必须断网) 使用注册...

2017.1.24学习记录

---恢复内容开始--- 一: 安装openjtag驱动,usb串口驱动,安装oflash,recureCRT软件。 二: 2.1 通过openjtag通过cmd.exe烧写bin文件,通过jlink烧写bin文件。烧写u-boot.bin 小心:用nandflash烧写时选块0 2.2 注意nand flash(openjtag)与nor flash(j...