text/template 代码阅读: exec

Go语言基础知识:结构体、指针、接口及反射详解
本文详细阐述了Go语言中的结构体赋值、指针、nil指针的行为,接口的使用限制以及方法调用规则。还讨论了接口的值与类型、反射Value的特性和用法,包括ValueOfnil的情况。文章通过实例展示了在Go中如何处理这些概念,以及可能出现的陷阱和错误。

版权

本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.youkuaiyun.com/big_cheng/article/details/131683131.

前言

go1.19

prepare

struct

struct 赋值是copy

type TS_S1 struct {
	I int
}

func ts_1() {
	s1 := TS_S1{1}
	var s2 TS_S1 = s1
	s1.I = 2
	println(s1.I, s2.I) // 2 1
}

pointer

nil pointer 本身有地址

// pointer (的值, 即其存储的地址) 和pointer(变量) 本身的地址 含义不同.
// (pointer 0x0 值代表指向nil.)
func tp_1() {
	var p *int = nil
	fmt.Printf("%p %v %p\n", p, p, &p) // 0x0 <nil> 0xc00000a028

	var i = 3
	p = &i
	fmt.Printf("%p %v %p\n", p, p, &p) // 0xc00000e0d0 0xc00000e0d0 0xc00000a028 // (地址不变, 值变了)
}

嵌套

pointer 类型可嵌套:

func tp_1b() {
	i := 1
	pi := &i
	p := &pi
	fmt.Printf("%T\n", p) // **int
}

ref/spec#Method_declarations: “the receiver. Its type must be a defined type T or a pointer to a defined type T … A receiver base type cannot be a pointer or interface type”.

type TP_PI *int

// method receiver 的base type 不能是pointer.
// (pointer 可嵌套, 但嵌套后不能再定义方法 - 为了简单.)
// func (TP_PI) M1()  {}
// func (*TP_PI) M2() {}

上面2个方法定义的receiver base type 都是TP_PI - 是pointer 类型, 所以不许.

type MyInt int

func (*MyInt) M3() {}

上面receiver base type 是MyInt - 不是pointer 类型, 所以可以.

calling a method on a nil pointer can work

type TP_S2 struct {
	I int
}

func (*TP_S2) M2() {}

func tp_4() {
	var p *TP_S2
	p.M2()
	// println(p.I) - panic of nil
}

interface

nil interface 有地址

func ti_1() (err error) {
	fmt.Printf("addr %p => nil %t\n", &err, err == nil) // addr 0xc0000422e0 => nil true
	return nil
}

不能给interface 再定义方法

type TI_I1 interface {
	M1()
}

// (method receiver 的base type 不能是interface.)
// func (TI_I1) M2()  {}
// func (*TI_I1) M3() {}

calling a method on a nil interface can’t work

func ti_1c() {
	var i TI_I1
	i.M1() // panic of nil
}

赋值给interface 的、和type assertion 产生的, 都是copy

func (TS_S1) Read(p []byte) (int, error) { return len(p), nil }

func ti_2() {
	s := TS_S1{1}
	var r io.Reader = s
	s.I = 2
	s2 := r.(TS_S1)
	s2.I = 3
	println(s.I, (r.(TS_S1)).I, s2.I) // 2 1 3
}

concrete

// interface 内部包含一个concrete (值与类型).
// interface 赋值给interface, 实际赋的是concrete copy.
// 即concrete 不会是interface - 也即interface 不能嵌套interface.
func ti_3() {
	s := TS_S1{1}
	var r io.Reader = s
	var i any = r // (赋的不是r, 而是其concrete copy)
	s.I = 2
	println(s.I, (i.(TS_S1).I))           // 2 1
	fmt.Printf("%s\n", reflect.TypeOf(i)) // tmpl.TS_S1 (any 记录了concrete 的类型)
}

concrete 的值与类型:

// interface 的concrete:
//
//	值为untyped nil/interface nil 时, 没有类型.
//	值为其他nillable-typed nil 时, 有类型 - 这与reflect.TypeOf 行为一致.
func ti_4() {
	var i any = nil
	var r io.Reader = nil
	fmt.Printf("%t %t %t\n", // true
		reflect.TypeOf(i) == nil,
		reflect.TypeOf(r) == nil,
		reflect.TypeOf((io.Reader)(nil)) == nil,
	)

	fmt.Printf("%t %t %t %t/%t %t\n", // false
		reflect.TypeOf((chan int)(nil)) == nil,
		reflect.TypeOf((func() int)(nil)) == nil,
		reflect.TypeOf((map[string]int)(nil)) == nil,
		reflect.TypeOf((*int)(nil)) == nil,
		reflect.TypeOf((*any)(nil)) == nil,
		reflect.TypeOf(([]int)(nil)) == nil,
	)
}

reflect.Value

concrete

// Value 类似interface, 也是内部包含一个concrete (值与类型).
// 例如Value.Type() 实际是指该concrete 的类型.
func tr_1() {
	v := reflect.ValueOf(2)
	fmt.Printf("%s\n", v.Type()) // int
	println(v.Interface().(int)) // 2
}

zero Value

// Value invalid 代表从未设置过concrete.
//
// Zero(ValueType) 返回的value 设置了concrete = 另一个invalid 的value.
// 注: v.IsZero() 是针对concrete 而非v 自己.
//
// 另, 上述也表明: value 可嵌套value.
func tr_2() {
	var v reflect.Value                   // 或 v := reflect.Value{}
	fmt.Printf("valid %t\n", v.IsValid()) // (Invalid)

	typ := reflect.TypeOf((*reflect.Value)(nil)).Elem()
	fmt.Printf("%s\n", typ) // reflect.Value
	v = reflect.Zero(typ)
	// reflect.Value (struct): valid true zero true
	fmt.Printf("%s (%s): valid %t zero %t\n",
		v.Type(), v.Kind(), v.IsValid(), v.IsZero())
	//
	v = v.Interface().(reflect.Value)
	fmt.Printf("%t\n", v.IsValid()) // (Invalid)
}

(重要)

ValueOf nil

// ValueOf untyped nil/interface nil 结果invalid.
//
// 对其他nillable-typed nil 结果valid - 即concrete 有类型无值(为nil).
// (注: 包括chan/func/map/pointer/slice).
func tr_3() {
	// (Invalid)
	println(
		reflect.ValueOf(nil).IsValid(),
		reflect.ValueOf((any)(nil)).IsValid(),
		reflect.ValueOf((io.Reader)(nil)).IsValid(),
	)

	// (Valid)
	v := reflect.ValueOf((map[string]int)(nil))
	fmt.Printf("%t, %s\n", v.IsNil(), v.Type()) // true, map[string]int

	/* 注: Zero(interface) 是返回的value 的concrete = nil interface,
	       和ValueOf(nil interface) 结果不同.
	typ := reflect.TypeOf((*io.Reader)(nil)).Elem()
	v = reflect.Zero(typ)
	fmt.Printf("%t, %s\n", v.IsNil(), v.Type()) // true, io.Reader
	*/
}

另,

// TypeOf 返回nil 对应 ValueOf 返回invalid.
func tr_4() {
	t := reflect.TypeOf((any)(nil))
	println(t == nil) // true

	t = reflect.TypeOf((map[string]int)(nil))
	fmt.Printf("%s\n", t) // map[string]int
}

一个可能的编译器bug

// 可能是编译器bug: 改成用同一个变量v, 则打印 true - 不对!
func tr_elem() {
	i := reflect.Value{}
	v := reflect.ValueOf(&i)
	v = v.Elem() // 返回的v 的concrete 对应i
	v = v.Interface().(reflect.Value)
	println(v.IsValid()) // false
}

(Elem() 应该总是把目标对象包装在一个Value 里返回 - 在本例里就是Value 嵌Value.)

Value 嵌套的例子

// 如果map 包含, 返回valid Value - 其concrete 是另一个invalid Value.
func tr_mapIdx() {
	m := map[string]reflect.Value{
		"i1":  reflect.ValueOf(1),
		"foo": {},
	}
	vm := reflect.ValueOf(m)

	v := vm.MapIndex(reflect.ValueOf("foo"))
	println(v.IsValid()) // true
	v = v.Interface().(reflect.Value)
	println(v.IsValid()) // false
}

Value 嵌套interface

type TR_I1 interface {
	M1()
}

type TR_MAP map[string]any

func (TR_MAP) M1() {}

func tr_map2() {
	m := TR_MAP{"k1": 1}
	var tr TR_I1 = m
	var i any = tr // 实际赋的是m copy
	m["k1"] = 2

	vM := reflect.ValueOf(i)
	fmt.Printf("%s\n", vM.Type()) // tmpl.TR_MAP
	// vK1 的concrete 类型是any
	vK1 := vM.MapIndex(reflect.ValueOf("k1"))
	println(vK1.Kind().String(), vK1.Elem().Int()) // interface 2

	// vK1 的concrete (any) 的concrete: int/2
	i = vK1.Interface()
	println(i.(int)) // 2
}

(map 赋值, 其elements 实际还是同一份 - 并未copy.)

exec.go

eval

evalPipeline

func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value reflect.Value)

for _, cmd := range pipe.Cmds {
    value = s.evalCommand(dot, cmd, value)
pipeline 由一组cmd 串联而成, 依次执行, 前一个cmd 的结果value 作为后一个cmd 的追加参数.

evalCommand

func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final reflect.Value) reflect.Value

cmd = arg0 arg1 arg2 …
例如: .Method1 .Key1 .Field2.Field3

arg0 如果是Field/Chain/Identifier/Pipe/Variable 节点, 则转到对应方法.
注: notAFunction 方法检查cmd 不是个方法调用: 只有arg0 且无final (== missingVal).

其他节点均不是方法调用: Bool/Dot/Nil/Number/String.

errorf 自己会panic, 后面再加panic 使意图更明确.

evalFieldNode

func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value

field = args[0].
args[1:] 和final 携带可能的其他参数, 因为field 可能是个方法如: .Method1 .Key1 123 nil.

直接转到evalFieldChain.

evalChainNode

func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value

ChainNode = FieldNode 前加一个(非Field/Identifier/Variable) 节点 - 一般这是一个pipe 节点: (pipe).Field1.Field2. 但不做假定, 调用通用的evalArg 求值:
pipe := s.evalArg(dot, nil, chain.Node)
(注: 类型传nil - 目前不知.)

再转到evalFieldChain (以pipe 为receiver).

evalFieldChain

func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ident []string, args []parse.Node, final reflect.Value) reflect.Value

.X.Y.Z arg1 arg2 … 形式. dot 用于arg, receiver 用于chain (.X.Y.Z).

chain 的各级存储在ident, 从首个到次末个(.X, .Y) 依次调用evalField 求值. chain 的前面各级肯定没有参数, 所以传nil 和missingVal. receiver 逐次更新.

最末ident (.Z) 求值时传递args 和final.

evalField

func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value

求receiver.fieldName 的值. 可能有arg (此时fieldName 是方法名) - 用dot 求其值.

如果receiver 是pointer 或interface, 连续去掉所有的pointer 和interface 返回最里层的concrete (注: interface 不能嵌套所以应该只有一层).
阻止"calling a method on a nil interface can’t work" 情况.

首先将fieldName 当作方法名来处理.
receiver 已经dereference.
因为待调用方法可能定义在&receiver 上, 再indirect 一次(在*T 上可以同时看到T 和*T 的方法), 但也阻止"不能给interface 再定义方法" 和"A receiver base type cannot be a pointer" 2种情况.
如果能找到该方法, 转到evalCall. 否则不是方法, 往下.

不是方法时hasArgs 应该为false.

  • receiver 是struct: 如果成功取到该字段的值但hasArgs 则报错 - 因为这里要直接返回字段值不论其是否方法, 所以不能有参数.
  • receiver 是map: 用fieldName 做key 取map 对应元素. 如果不存在则按option.missingKey 配置的方式处理.
  • receiver 是pointer: indirect 方法返回pointer 时应该isNil==true. 特殊处理如果pointer 是指向struct 且能找到fieldName 字段, 则改为报错"nil pointer evaluating …".

其他情况报错"can’t evaluate field …".

evalVariableNode

func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value

$x.Field1.Field2… 形式.

取出$x 的值, 再转到evalFieldChain.

walkRange

func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode)

evalPipeline 后indirect, 以防待遍历对象嵌套在Value 里面.

oneIteration:
因为evalPipeline 里已经定义了变量, 所以oneIteration 里可以直接setTopVar.

区分val 的类型: Array/Slice/Map/Chan 来遍历并调用oneIteration. 如果没有一条数据则再处理ElseList.

isTrue

func isTrue(val reflect.Value) (truth, ok bool)

在模板里一个reflect.Value val 是否为真. 不支持大类UnsafePointer. 对于数值是非0, 集合(array/map/slice/string) 是长度>0, nillable-type (chan/func/pointer/interface) 是非nil, struct 总是真.

call

evalCall

func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value

调用函数或方法fun, 名称为name - node (即args[0]) 的一部分.

首先检查fun 参数的个数(比较绕).

内置函数and/or 直接在这里完成处理. 见funcs.go, 这2个函数的参数类型是reflect.Value (即argType), 所以evalArg 求出的结果是Value 嵌套一个Value. .Interface().(reflect.Value) 取到嵌套的Value.
如果短路处理到final, final 已经有值, 只需再校验类型.

下面组装参数值数组(同样较绕).

最终调用funcs.go 的safeCall 完成函数调用.

注-unwrap: 因为Value.Call 返回[]Value, 如果被调用函数本身是返回Value, 则将返回Value 嵌套Value, unwrap 主要用在这种情况下剥离外层的Value.

validateType

func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value

检查value 可否赋给类型typ 的变量.

value Invalid 时返回Invalid、或nillable-type 的nil 值、或特殊地对struct (non-nillable) 仅支持reflect.Value 类型 - 返回Zero(ValueType) - 与后面evalArg 方法里一致.

value Valid 时, 额外自动转换:

  • value 是interface 且其concrete 能赋给typ, 则返回concrete
  • value 是pointer 且其base type 能赋给typ, 则返回其指向的对象(dereference 一次)
  • 指向value 类型的pointer 类型能赋给typ, 则返回指向value 的指针 (indirection 一次)

注意后2种dereference/indirection 只做一次, 不像在方法调用时持续做(见indirect 方法), 因为实践来看在这里一次即可.

evalArg

func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value

给出任一arg 节点n, 及其类型typ (可能nil), 求值. 有2种情况:

  • (pipe).Field1.Field2: 求pipe 值, typ nil
  • 函数/方法调用如.Method arg1 arg2: 求arg 值, typ non-nil

与evalCommand 方法较类似, 先处理可能带参数的节点类型(Field/Chain 等) 但增加类型dot/nil:

  • dot: 对 (.).Field1, typ nil, 所以不能放到后面(switch typ.Kind()) 处理
  • nil: 单个的nil 不能做cmd 节点 (见evalCommand 方法), 所以这是对函数调用求参数值, typ non-nil, 可以放到这里(switch arg := n.(type)) 处理

再switch typ.Kind() 处理(typ non-nil).
例如typ 大类是bool, 则evalBool 要求节点n 一定要是BoolNode.
增加any 和reflect.Value 大类的处理.

evalEmptyInterface

func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value

求函数调用的参数值, 参数类型any 或reflect.Value.

与evalArg 方法相比少了ChainNode - 因为在evalArg 前半段已处理.

indirect

func indirect(v reflect.Value) (rv reflect.Value, isNil bool)

循环dereference pointer/interface, 直至其指向的或concrete 是nil.

注意: 返回的isNil false 可能不准, 例如用一个nil slice 调用也返回false. 只有返回的isNil true 才是准的.

printableValue

func printableValue(v reflect.Value) (any, bool)

注-“return v.Interface()”: 如果v 嵌套interface 则返回该interface, fmt 对interface 会打印其concrete.

补充

function 何时会执行, 何时不会?

见evalField 方法. 如果receiver 上能找到该方法, 则会执行. 否则receiver 作为struct 拥有该方法(为field) 或作为map 拥有该方法(为elem) 都会直接返回而非执行该方法.
(ref/spec#Method_declarations: If the base type is a struct type, the non-blank method and field names must be distinct.)

例如TS_S1 类型定义Add 方法:

func (s TS_S1) Add(i ...int) int

则下面模板会执行该方法2次(.Key1 求值为TS_S1 对象):

a {{.Key1.Add .Key1.Add}} b

一个遗漏检查Value concrete 值为nil 的bug

funcs.go addValueFuncs 方法用于添加用户自定义函数:

func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
    for name, fn := range in {
        ......
        v := reflect.ValueOf(fn)
        if v.Kind() != reflect.Func { ...... }
        if !goodFunc(v.Type()) { ...... }
        out[name] = v
    }
}

这里检查了v.Kind() 和v.Type(), 但遗漏检查v.IsNil() - 因为func 类型是nillable-type, Value concrete 可以类型是func 但值为nil.

	t := template.New("P")
	t.Funcs(template.FuncMap{
		"foo": (func() int)(nil),
	})
	t.Parse("a {{call foo}} b")

上面模板可以Parse, 但执行时evalCall => safeCall => fun.Call(args) 会报错: call of nil function!

<template> <view class="u-wrap"> <view class="u-menu-wrap"> <scroll-view scroll-y scroll-with-animation class="u-tab-view menu-scroll-view" :scroll-top="scrollTop"> <view v-for="(item,index) in tabbar" :key="index" class="u-tab-item" :class="[current==index ? 'u-tab-item-active' : '']" :data-current="index" @tap.stop="swichMenu(index)"> <text class="">{{item.name}}</text> </view> </scroll-view> <block v-for="(item,index) in tabbar" :key="index"> <scroll-view scroll-y class="right-box" style="height: 800rpx;" v-if="current==index"> <view class="page-view"> <view class="" style="position: relative; width: 550rpx;" v-for="(item1, index1) in item.foods" :key="item1.id"> <view class="item u-border-bottom flex justify-start align-center " style="height: 200rpx;"> <view class="" @click="goShop(item1.id)"> <u-image :src="item1.images" height="152" width="152"></u-image> </view> <view class="margin-left-sm" style="width: 392rpx;"> <view class=" flex justify-between align-center" @click="goShop(item1.id)"> <view class="text-bold" style="font-size: 30rpx;"> {{item1.title}} </view> </view> <view class="flex text-sm align-center justify-start margin-top-xs" style="color: #D27E1B;"> <view class="padding-right-xs"> 评分:{{item1.score}} </view> <view class=""> 月售:{{item1.sell}} </view> </view> <view class="text-sm padding-tb-xs" style="color: #999999;"> 月浏览量:{{item1.browse}} </view> <view class="flex justify-between align-center"> <view class="flex justify-start align-center "> <view class="text-red " style="font-size: 24rpx;"> ¥{{item1.newMoney}} </view> <view class="text-gray padding-lr-xs" style="text-decoration: line-through"> ¥{{item1.newMoney}} </view> <view class=" flex align-center justify-center" style="font-size: 20rpx; height: 36rpx; width: 70rpx; border: 2rpx solid #FF4848;color: #FF4848; border-radius: 12rpx;"> {{item1.discount}}折 </view> </view> <view class="flex align-center justify-end"> <view class="flex align-center" v-if=" item1.buySum >0"> <!-- 减号按钮 --> <view v-if="getBuySum(item1) > 0" class="flex justify-center align-center" style="width: 36rpx; height: 36rpx; border-radius: 50%; background-color: #f0f0f0;" @click.stop="decrease(item1)"> <u-icon name="minus-circle" size="36rpx" color="#AAA8A7"></u-icon> </view> <!-- 数量显示 --> <text v-if="getBuySum(item1) > 0" class="margin-lr-xs" style="font-size: 28rpx; border-radius: 8rpx; background-color: #D9D9D9; color: #333; min-width: 40rpx; text-align: center;"> {{ getBuySum(item1) }} </text> <!-- 加号按钮 --> <view @click.stop="add(item1)" class="flex justify-center align-center" style="width: 36rpx; height: 36rpx; border-radius: 50%; background-color: #fff;"> <u-icon name="plus-circle" color="#FC7746" size="36rpx"></u-icon> </view> </view> <view class="" v-if="item1.buySum ==0" @click="add(item1)"> <u-icon name="plus-circle" color="#FC7746" size="36rpx"></u-icon> </view> </view> </view> </view> </view> </view> </view> </scroll-view> </block> </view> </view> </template> <script> import { customizeLabel, goodsInfo } from '@/api/shop.js' export default { name: 'CategoryGoodsList', props: { // 定义 shopId 属性 shopId: { type: Number, // 支持字符串或数字(比如 '123' 或 123) required: true, // 必传 validator(value) { return value !== '' && value != null; } } }, data() { return { scrollTop: 0, //tab标题的滚动条位置 current: 0, // 预设当前项的值 menuHeight: 0, // 左边菜单的高度 menuItemHeight: 0, // 左边菜单item的高度 // tabbar: [{ // "name": "品类名称", // "foods": [{ // "id": 1, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }, { // "id": 2, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }, { // "id": 3, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }, { // "id": 10, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }, { // "id": 11, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }, // ] // }, // { // "name": "品类名称", // "foods": [{ // "id": 4, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }] // }, // { // "name": "品类名称", // "foods": [{ // "id": 5, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // } // ] // }, // { // "name": "品类名称", // "foods": [{ // "id": 6, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }] // }, // { // "name": "品类名称", // "foods": [{ // "id": 7, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }] // }, // { // "name": "品类名称", // "foods": [{ // "id": 8, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }] // }, // { // "name": "品类名称", // "foods": [{ // "id": 9, // "title": "镇江陈醋500ml", // "images": "https://env-00jxta4kb22c.normal.cloudstatic.cn/1/static/my/collect/1.png", // 'score': "5分", // "sell": "1000+", // "browse": '453456', // "newMoney": 45.56, // "money": 68, // "discount": 6.7, // 'buySum': 0 // }] // }, // ], tabbar: [{ "name": '', "foods": [] }], tab: [], goods: [] } }, computed: { }, methods: { generateTabbar(labelData, goodsList) { console.log("数据准备", this.tab) // 步骤1:创建 { id: labelName } 映射,并初始化 tabbar const tabbar = labelData.map(label => ({ name: label.labelName, foods: [] })); console.log("第一步", tabbar) // 步骤2:构建 id -> index 的映射,便于快速定位 const labelMap = {}; labelData.forEach((label, index) => { labelMap[label.id] = index; }); console.log("第二部", labelMap) // console.log("商品数据", goodsList) // 步骤3:遍历商品,按 customizeLabelId 分类 goodsList.forEach(good => { const labelId = good.customizeLabelId; // 如果没有分类或该分类不存在,则跳过 if (!labelId || !labelMap.hasOwnProperty(labelId)) { console.warn(`商品 "${good.name}" 无有效分类,已忽略`); return; } const tabIndex = labelMap[labelId]; const foodItem = { id: good.id, title: good.name, images: good.imageUrl, // 默认图 // score: "5分", sell: good.monthlySales, browse: good.browseCount, newMoney: parseFloat(good.price.toFixed(2)), money: good.price, // 假设原价高1.5倍 // discount: parseFloat((good.price / (good.price * 1.5) * 10).toFixed(1)), // 打几折 buySum: 0 }; tabbar[tabIndex].foods.push(foodItem); }); console.log("返回的数据", tabbar) return tabbar; }, async getCustomizeLabel() { this.loading = true; try { const data = { storeId: this.shopId, pageSize: -1 } const response = await customizeLabel(data); if (response.code === 200 || response.code === 0) { this.tab = response.data.list } else { console.error('接口返回错误:', response.message); } } catch (error) { console.log("请求失败", error) } finally { this.loading = false; } const goodsRes = await this.getGoodsInfo(); // 注意:要等这个完成 console.log("商品数据222222222222222222222", goodsRes) console.log("商品数据666666666666666", this.goods) this.tabbar = this.generateTabbar(this.tab, this.goods); this.$emit('update:tabbar', this.tabbar); }, async getGoodsInfo() { this.loading = true; try { const data = { storeId: this.shopId, pageNo: 1, pageSize: -1 } const response = await goodsInfo(data); if (response.code === 200 || response.code === 0) { this.goods = response.data.list } else { console.error('接口返回错误:', response.message); } } catch (error) { console.log("请求失败", error) } finally { this.loading = false; } }, add(item) { const current = this.getBuySum(item); this.$set(item, 'buySum', current + 1); this.updateCart(); }, // 减少商品数量 decrease(item) { const current = this.getBuySum(item); if (current <= 0) return; this.$set(item, 'buySum', current - 1); this.updateCart(); }, updateCart() { const cartItems = []; let totalAmount = 0; let totalPrice = 0; this.tabbar.forEach(category => { category.foods.forEach(food => { const buySum = this.getBuySum(food); if (buySum > 0) { const itemTotal = buySum * parseFloat(food.newMoney); cartItems.push({ id: food.id, title: food.title, images: food.images, oldMoney: food.money, buySum, price: parseFloat(food.newMoney), subtotal: itemTotal }); totalAmount += buySum; totalPrice += itemTotal; } }); }); // 将计算结果格式化并提交到 Vuex const cartData = { cartItems, totalAmount, totalPrice: parseFloat(totalPrice.toFixed(2)) }; // 提交到 Vuex 的 mutation this.$store.commit('SET_CART_DATA', cartData); console.log('数据更新', cartData) }, // 安全获取 buySum 值 getBuySum(item) { return typeof item.buySum === 'number' && !isNaN(item.buySum) ? item.buySum : 0; }, goShop(id) { // console.log('id', id) uni.navigateTo({ url: `/pages/shop/shop?id=${id}` }) }, getImg() { return Math.floor(Math.random() * 35); }, // 点击左边的栏目切换 async swichMenu(index) { if (index == this.current) return; this.current = index; // 如果为0,意味着尚未初始化 if (this.menuHeight == 0 || this.menuItemHeight == 0) { await this.getElRect('menu-scroll-view', 'menuHeight'); await this.getElRect('u-tab-item', 'menuItemHeight'); } // 将菜单菜单活动item垂直居中 this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2; }, // 获取一个目标元素的高度 getElRect(elClass, dataVal) { new Promise((resolve, reject) => { const query = uni.createSelectorQuery().in(this); query.select('.' + elClass).fields({ size: true }, res => { // 如果节点尚未生成,res值为null,循环调用执行 if (!res) { setTimeout(() => { this.getElRect(elClass); }, 10); return; } this[dataVal] = res.height; }).exec(); }) } }, mounted() { // this.getShopList() this.getCustomizeLabel() // console.log("我是最终数据", this.storeData) } } </script> <style lang="scss" scoped> .u-wrap { // height: calc(100vh); /* #ifdef H5 */ // height: calc(100vh - var(--window-top)); /* #endif */ display: flex; flex-direction: column; } .u-menu-wrap { flex: 1; display: flex; overflow: hidden; // border-radius: 20rpx; } .u-tab-view { width: 200rpx; height: 100%; // border-radius: 20rpx; } .u-tab-item { height: 110rpx; background: #fff; box-sizing: border-box; border-radius: 20rpx 20rpx 0 0; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #444; font-weight: 400; line-height: 1; } .u-tab-item-active { position: relative; color: #fff; // font-size: 30rpx; // font-weight: 600; background: #E96F50; } .u-tab-view { height: 100%; } .right-box { background-color: rgb(250, 250, 250); } .page-view { padding-left: 16rpx; padding-right: 16rpx; .buy { height: 48rpx; width: 112rpx; color: #fff; border-radius: 24rpx; background-color: #FC7746; } } .class-item { margin-bottom: 30rpx; background-color: #fff; padding: 16rpx; border-radius: 8rpx; } .item-title { font-size: 26rpx; color: $u-main-color; font-weight: bold; } .item-menu-name { font-weight: normal; font-size: 24rpx; color: $u-main-color; } .item-container { display: flex; flex-wrap: wrap; } .thumb-box { width: 33.333333%; display: flex; align-items: center; justify-content: center; flex-direction: column; margin-top: 20rpx; } .item-menu-image { width: 120rpx; height: 120rpx; } </style> 购买数据可以从vuex中获取然后回显点击切换店铺后切换回来购物车里的数据和店铺商品一致
11-22
<template> <view class="container"> <view class=" ditu"> <map id="map" style="width:100%;height:100%;" :latitude="latitude" :longitude="longitude" :scale="scale" :markers="markers" :include-points="includePoints" :rotation="rotate" enable-traffic show-location="true" enable-poi="true" show-compass enable-3D="true" @markertap="toMap()"> </map> </view> <view class="shijianzhou-box"> <view class="header"> <u--text :text="numName" mode="text" size="30" bold="true"></u--text> </view> <view class="fangxiang"> <u--text :text="`${stations[0].name}→${stations[stations.length-1].name}`" mode="text"></u--text> <u--text text="首班:07:30--末班:19:30" mode="text"></u--text> </view> <!-- 横向时间轴 --> <u-scroll-list :indicator="false" :scroll-left="scrollLeft" scroll-with-animation class="shijianzhou"> <u-steps direction="horizontal" :current="currentIndex" active-color="#1A73E8" inactive-color="#666"> <u-steps-item v-for="(station, index) in stations" :key="station.id" :title="station.name"> </u-steps-item> </u-steps> </u-scroll-list> </view> </view> </template> <script> import QQMapWX from "@/common/qqmap-wx-jssdk1.2/qqmap-wx-jssdk.min.js"; export default { data() { return { currentIndex: 0, // 当前站点索引 contentScrollW: 0, // 导航区宽度 scrollLeft: 0, // 横向移动距离 stations: [], // 公交站点信息 numName: '', // 公交线路名 latitude: 39.08692370970444, //固定纬度 longitude: 121.83193249495616, //固定经度 scale: 16, //地图缩放程度 markers: [{ id: 1, latitude: 39.08692370970444, longitude: 121.83193249495616, iconPath: '/static/bus.png', width: 30, height: 30, rotate: 0 }], includePoints: [{ // latitude: 39.08612370970444, // longitude: 121.83193249495616 }], // 新增加包含点数组 rotate: 0 // 新增方向角度 } }, mounted() { // 获取标题区域宽度,和每个子元素节点的宽度 this.getScrollW() }, methods: { selectStation(index) { this.currentIndex = index this.scrollLeft = this.stations[index].left - this.contentScrollW / 2 + this.stations[index].width / 2 - 33; }, // 获取标题区域宽度,和每个子元素节点的宽度以及元素距离左边栏的距离 getScrollW() { const query = uni.createSelectorQuery().in(this); query.select('.shijianzhou').boundingClientRect(data => { // 拿到 scroll-view 组件宽度 this.contentScrollW = data.width }).exec(); query.selectAll('.timeline-item').boundingClientRect(data => { let dataLen = data.length; for (let i = 0; i < dataLen; i++) { // scroll-view 子元素组件距离左边栏的距离 this.stations[i].left = data[i].left; // scroll-view 子元素组件宽度 this.stations[i].width = data[i].width } }).exec() }, getLocation() { // uni.getLocation({ // type: 'gcj02', // altitude: true, // 获取高度信息 // isHighAccuracy: true, // 高精度模式 // highAccuracyExpireTime: 3000, // 高精度超时时间 // /* type: 'wgs84', */ // success:(res) => { // console.log('当前位置:'); // console.log('经度:',res.latitude); // console.log('纬度:',res.longitude); // this.latitude = res.latitude // this.longitude = res.longitude // // 设置标记点 // this.markers = [{ // id: 1, // latitude: res.latitude, // /* latitude: 48.91369, */ // longitude: res.longitude, // /* longitude: 141.61476, */ // iconPath: '/static/bus.png', // width: 30, // height: 30, // rotate: res.heading || 0 // 使用设备方向 // }]; // this.includePoints = [{ // latitude: res.latitude, // longitude: res.longitude // }] // this.rotate = res.heading || 0 // }, // fail(err) { // console.error('获取位置失败', err); // uni.showToast({ // title: '定位失败,请检查权限设置', // icon: 'none' // }); // } // }); }, }, // onLoad: function(option) { // // 先设置默认值 // this.latitude = 39.08752370970444; // this.longitude = 121.83193249495616; // // 然后获取定位 // this.getLocation(); // // 最后加载站点数据 // if (option.items) { // const item = JSON.parse(decodeURIComponent(option.items)); // this.stations = item.stopover // this.numName = item.num // } // } } </script> <style lang="scss" scoped> .slot-icon { width: 20px; height: 20px; background-color: $u-warning; border-radius: 10px; font-size: 12px; color: #fff; line-height: 21px; text-align: center; } .container { background: #ffffff; position: relative; height: 100vh; } .coordinate-box { position: absolute; right: 20rpx; bottom: 50%; width: 80rpx; height: 80rpx; background-color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1); } .shijianzhou-box { /* background-color: #eee; */ background-color: #fff; border-radius: 25rpx; position: absolute; left: 20rpx; /* 距离左侧20rpx */ right: 20rpx; /* 距离右侧20rpx */ z-index: 999; } .ditu { width: 100vw; height: 50vh; } </style>为什么报错[渲染层错误] Uncaught Error: 参数错误:LatLng 传入参数 (NaN, NaN) 非合法数字。(env: Windows,mp,1.06.2504010; lib: 3.8.10)
07-10
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值